SqDRIFT 기저 상태 추정 알고리즘
이 튜토리얼을 직접 실행해 보려면 ‘ Python ’ 노트북을 다운로드하세요. 노트북에는 FCIDump 입력 파일이 아직 없는 경우, 해당 파일을 다운로드합니다.
예상 소요 시간: Heron r3 프로세서 기준 180초 (참고: 이는 단지 예상치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)
학습 성과
- 트로터화 방식에 비해 더 얕은 회로를 설계하는 방법을 알아보세요
- qDRIFT 와 SQD를 활용한 기저 상태 추정 전체 워크플로우를 살펴봅니다
- 다른 Qiskit 애드온과 함께
qiskit-fermions사용하여 이러한 워크플로를 구현하는 방법을 알아보세요
전제조건
- ‘샘플 기반 양자 대각화(SQD)’ 개요 읽어보기
- '샘플 기반 크릴로프 양자 대각화(SKQD) ' 강의 읽기
배경
SqDRIFT 이는 SKQD의 변형으로, 비트스트링을 샘플링할 안자츠(ansatz)를 선택할 필요성을, 대상 해밀토니안으로부터 직접 구성된 시간 진화 회로 집합으로 대체한 것이다. 이는 해밀토니안의 계수를 바탕으로 해밀토니안에서 더 작은 시간 진화 연산자를 하위 표본 추출함으로써 달성되며, 이를 ‘ qDRIFT ’ 트로터화 방법이라고 한다.
두 구현 방식 모두 Qiskit Fermions를 사용하여 분자 해밀토니안을 표현하고, ‘ qDRIFT ’ 알고리즘을 위한 회로를 구성합니다. Python 구현은 페르미온 트랜스파일러 단계를 사용하는 반면, C++ 구현은 그룹을 명시적으로 샘플링하여 큐비트 연산자로 매핑합니다.
해밀토니안이 다음과 같은 형태라고 하자:
여기서, 일반성을 잃지 않고, 를 만족하며, 의 최대 고유값의 절댓값이 와 같다고 가정한다. 부호가 있는 또는 복소수인 모든 선인자는 로 흡수되므로, 계수 는 엄격히 양의 가중치이며, 는 각 항의 방향을 나타낸다. 여기서 는 해밀토니안의 항의 개수(또는, 그룹화 후의 그룹 개수)를 나타내며, 이는 해밀토니안의 속성으로서, 아래에서 로 표기된 단일 회로에 샘플링된 연산자의 개수와는 구별된다.
그런 다음, qDRIFT 알고리즘은 목표 시간 에 대해, 어떤 연산자 를 구현하며, 여기서 는 에서 시작하여 다음과 같이 정의된 SqDRIFT 회로를 나타냅니다:
여기서 는 회로당 표본 추출된 연산자의 수이며, 는 앙상블 내의 회로 수입니다. 이 제품은 의 추출 결과를 기반으로 하며, 의 모든 해밀토니안 항을 대상으로 하는 것은 아닙니다. 또한 항들이 중복 허용 방식으로 추출되므로, 동일한 가 단일 내에서 두 번 이상 나타날 수 있습니다.
수량:
이는 계수들의 노름이므로, 단계 각각은 어떤 항이 추출되었는지에 관계없이 동일한 지속 시간 동안 진행됩니다. 계단 각도의 균일성은 qDRIFT: 의 특징이며, 계수(coefficient)는 해당 항이 회전되는 각도가 아니라, 그 항이 그려지는 빈도를 통해 결과에 영향을 미칩니다. 지수는 다음 분포에서 표본 추출됩니다:
따라서 수열 는 이 분포에서 추출된 항 지수의 무작위 순서열입니다. 가 양수이며 합이 이므로, 이는 정규화된 확률 분포이며, 무작위 추출에 대한 결과 채널의 기대값은 하에서의 진화를 근사하며, 가 커짐에 따라 오차는 줄어든다. 근사 오차는 항의 개수 가 아니라 에 따라 달라진다는 점에 유의하십시오.
( SqDRIFT 의 논문에서는 항의 수를 , 수열의 길이를 로 표기하고 있으나, 여기서는 두 가지를 명확히 구분하기 위해 와 를 사용합니다.)
이 튜토리얼에서는 이러한 무작위 회로들의 앙상블을 생성하는 방법을 설명합니다. 이러한 회로를 생성한 후, 서로 다른 연산자에 대해 크릴로프 부분공간을 생성하는 방식과 유사하게, 서로 다른 시간 매개변수를 갖는 여러 연산자로부터 비트열을 표본 추출합니다. 이를 통해 기저 상태 벡터와 샘플링된 비트열 간의 중첩도가 높아집니다.
요구사항
이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하십시오
- 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에 대하여. 이 파일은 최소 STO-3G 기저에서, 원자 간 거리가 1.09 인 질소 분자( )를 N2_sto_3g 설명하며, 이는 실험적으로 확인된 평형 결합 길이입니다. 이 방정식의 헤더에는 NORB=10, NELEC=14, 및 가 선언되어 있습니다 MS2=0: 10개의 공간 궤도(따라서 20개의 스핀 궤도, 그리고 조던-위그너 표기법 하에서 20개의 큐비트), 스핀 싱글렛에 속한 14개의 전자, 즉 7개의 전자와 7개의 전자입니다. 모든 궤도에 대칭 표식 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 궤도에 따라 달라지기 때문에, 재생성된 파일은 궤도 위상이나 순서 면에서 제공된 파일과 다를 수 있으나, 총 에너지에는 영향을 미치지 않습니다.
파일 가져오기. GitHub 저장소에서 FCIDump를 찾아보세요. 아래 코드를 실행하면 튜토리얼의 나머지 부분에서 요구하는 위치로 해당 파일을 가져올 수 있습니다.
먼저 pyscf에서 제공하는 cisolver 기능을 사용하여 기준 에너지를 구합니다. 이것이 바로 우리가 다루고 있는 분자의 진정한 기저 상태 에너지입니다. 이를 위해 먼저 궤도 수와 전자 수를 각각 나타내는 와 norb nelec를 정의하겠습니다. 그런 다음 h2e, 각각 1전자 적분과 2전자 적분을 나타내는 와 h1e 를 정의합니다. 이 모든 것들은 나중에 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해밀토니안 불러오기
필요한 데이터가 준비되면, FCI 파일에서 다음 형식과 호환되는 해밀토니안을 읽어옵니다. qiskit-fermions
fcidump = FCIDump.from_file(name)
hamiltonian = FermionOperator.from_fcidump(fcidump)
num_modes = 2 * fcidump.norb다음과 함께 사용하는 페르미온 워크플로 qiskit-fermions
먼저 qiskit-fermions, 트랜스파일러 패스와 페르미온 회로 전용 게이트를 제공하는 를 사용하여 해밀토니안을 페르미온 회로 모델로 매핑할 것입니다. 이들은 나중에 이 워크플로우에 대한 Qiskit의 기존 트랜스파일러 처리가 진행되기 전에 사용될 것입니다.
용어 그룹화
결과의 재현성을 보장하기 위해, 먼저 를 사용하여 항들을 구조만을 기준으로 정렬합니다 canonical_order . 따라서 목록 canon 내 연산자의 순서는 고정되어 있습니다. 이는 향후 사용할 ‘pass’가 QDriftTrotterization 무작위 인덱스를 추출하여 ‘ qDRIFT ’ 연산자를 생성하기 때문에, 생성된 연산자의 재현성을 보장합니다.
이 단계에서는 계수가 동일한 관련 항들을 묶음으로써, 전자 구조 해밀토니안에 존재하는 다양한 대칭성을 활용합니다. 이렇게 하면 ‘ qDRIFT ’ 프로토콜이 표본을 추출하는 연산자 계수 분포가 변경되지만, 이는 수렴성 보증에는 영향을 미치지 않습니다. 특히, 대칭성에 의해 관련이 있는 항들을 묶으면 파울리 항들이 유리하게 상쇄되며, 이러한 항들의 작용 하에서 상태의 시간 발전을 계산할 때 전체적인 회로 깊이가 줄어든다.
qiskit-fermions 이 작업을 대신 수행해 주는 group_terms_by_electronic_structure 함수를 제공합니다.
이 식은 정상 순서 항을 group_terms_by_electronic_structure 가정한다는 점에 유의하십시오.
대각선 항 필터링
회로를 생성하는 데 사용되는 해밀토니안에서 대각선 항들을 제거함으로써, qDRIFT 샘플링 슬롯이 구성 간에 인구(population)를 이동시키는 항들에 사용되도록 합니다. 이러한 항들은 다음 단계에서 Evolution 게이트가 구성되기 전인 이 시점에서 해밀토니안에서 제거하는 것이 가장 좋습니다.
문제의 항들은 직업-수 기저에서 대각선에 위치한 항들, 즉 수 연산자 의 곱들입니다. 이 정의에 해당하는 항은 세 가지 종류가 있습니다:
- 상수 에너지 오프셋, 즉 0차 수 연산자의 곱으로, 그 시간 진화는 전체 위상에만 기여하며;
- 와 같은 개별 수 연산자들로, 이들의 시간적 진화는 단일 큐비트 회전으로 환원되며;
- 와 같은 고차 곱.
이들 중 어느 것도 그 자체로는 직업-숫자 구성 간에 인구를 이동시키지 않으며, 이미 존재하는 구성의 단계에만 영향을 미칩니다. 그러나 이것들이 단순히 무의미한 것은 아닙니다. 이러한 상대 위상들은 회로 후반부의 여기 항에 의해 생성되는 간섭에 영향을 미치므로, 이를 필터링하면 실제로 생성되는 진화 과정이 바뀌게 되고, 샘플링 분포도 달라질 수 있습니다. 이는 회로 생성 단계에서 샘플링을 여기 항에 집중시키기 위해 의도적으로 적용된 근사법일 뿐, 샘플링된 분포를 그대로 두는 단계는 아닙니다. 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
페르미온 회로 만들기
이제 각 시간 단계에 대한 페르미온 회로를 생성하겠습니다. 각 회로는 앞서 정의한 진화 시간을 갖는 단일 진화 게이트로 구성됩니다. 진화 연산자는 해밀토니안이다. 이후 이러한 회로에 대해 트랜스파일러 처리를 수행하여 qDRIFT 회로를 생성합니다.
안슐츠 준비
우리는 클래스를 InitializeModes 사용하여 하트리-포크 상태를 준비합니다. 질소의 경우, 이 과정은 단순히 첫 번째 num_elec_a 큐비트에 X 게이트를 적용한 다음, 나머지 num_elec_b 큐비트에 X 게이트를 적용하는 것으로, 질소의 경우 두 경우 모두 7개입니다. 이 상태는 질소의 7개의 전자와 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 트랜스파일러 패스가 이 작업을 대신 수행해 줍니다. 이제 퀀텀 비트(qubit) 간 연결성이 제한적임에도 불구하고, 해밀토니안에 장거리 결합이나 2차 이상의 항이 포함되어 있더라도 하드웨어에서 더 효율적으로 실행될 수 있는 더 단순한 회로를 만들 수 있게 되었습니다. 기간별 그룹화 후, 가중치를 기준으로 연산자를 표본 추출합니다. 각 연산자 에 대해, 가중치 는 다음과 같이 정의된다:
1단계에서 항들이 그룹으로 묶였기 때문에, 여기에서 각 는 하나의 전체 그룹을 나타냅니다. 즉, 는 그룹 에 속한 항들의 평균 절대 계수이며, 이 그룹에 속한 각 항은 계수가 부호로만 축소된 형태로 표현됩니다.
페르미온 최적화 및 하드웨어 네이티브 최적화
이 함수는 를 입력으로 받아 FermionicCircuit , 하드웨어에서 실행되도록 트랜스파일링할 수 있는 최적화된 최종 회로를 생성하는 MultiStagePassManager 를 반환합니다 generate_preset_jw_pass_manager() . 기본 최적화 단계를, 우리의 패스가 QDriftTrotterization 포함된 FermionicPassManager 단계로 대체합니다:
- 이
QDriftTrotterization패스는 내부적으로 가중치 계산 및 샘플링 기능을 활용하여, 샘플링에 사용할 회로를 생성합니다 - 이
RelabelModes패스는 페르미온 모드를 재배열하여 큐비트 간의 연결성을 최적화하고 게이트 깊이를 줄일 수 있는 또 다른 최적화 단계입니다. 자세한 내용은 API 참조 문서를 참조하십시오
나머지 단계들은 자동으로 MultiStagePassManager 실행되며, 페르미온에서 큐비트로의 전체 매핑을 처리합니다:
- F2QLayout : 프리셋 패스 관리자는 해당 패스를
TrivialF2QLayout적용하며, 이는 의 페르미온 비트를 의 큐비트로 간단히 매핑합니다. - F2QSynth : 페르미온 기반 회로 명령어를 큐비트 기반 명령어로 매핑하는 트랜스파일레이션 단계.
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 primitives를 사용하여 실행하기
이제 회로가 준비되었으니, AerSimulator 에서 ‘ Qiskit primitives ’ 기능을 사용하여 회로를 실행해 볼 수 있습니다. 우리는 여러 선거구에서 나온 모든 득표수를 합산할 것입니다. 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 processing4단계: 후처리를 수행하고 원하는 기존 형식으로 결과를 반환합니다
SQD에 비트스트링 사용
이제 선택된 비트열에 대해 대각화 기법을 적용하여, 분자의 기저 상태 에너지에 해당하는 가장 작은 고유값을 구할 수 있습니다. 콜백 함수를 생성하고, 초기 점유율을 선언하며, 매개변수를 설정한 후, 마지막으로 대각화 방식을 실행합니다. 콜백 함수는 각 반복 단계에서 현재 반복 번호와 현재 고유값 추정치를 출력하는 데 사용됩니다.
마지막으로, 기저 상태의 추정치를 구하기 위해, 결과적인 에너지에 nuclear_repulsion_energy 를 더합니다.
참고 : 부분 공간의 차원은 잡음이 없는 시뮬레이터에서도 반복 과정 전반에 걸쳐 고정되어 있지 않습니다. 각 하위 표본은 서로 다른 구성 집합을 추출하며, 복원 단계에서 반복 과정 사이에 풀의 형태가 재구성되기 때문에, 보고된 차원은 하위 표본마다 달라집니다. 무잡음 표본 추출만으로는 선택된 부분 공간의 차원을 확정할 수 없다. 그러나 하드웨어 실행의 경우, 노이즈가 포함된 샷이 입자 수 대칭성을 깨뜨리고, 구성 복원 과정에서 이를 추가적인 기저 벡터로 변환하기 때문에 체계적으로 더 큰 부분공간이 생성되는 경향이 있다. 그 때문에 하드웨어 섹션에서는 비트스트링을 정리하기 위한 또 다른 단계를 소개할 예정입니다.
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를 실행하기 전에 유효하지 않은 비트스트링을 걸러낼 수도 있고, 그렇지 않고 바로 다음 단계로 진행할 수도 있습니다. 하드웨어 실행의 경우 일반적으로 가지치기를 생략하는 것이 바람직합니다. 이는 대칭성이 깨진 샷을 구성 복구 과정에서 활용할 수 있게 해주기 때문인데, 이를 통해 해당 샷을 유효한 구성으로 복구하여 해당 샷을 아예 버리는 대신 부분공간을 확장할 수 있기 때문입니다.
질소는 와 전자를 각각 7개씩만 가질 수 있으므로, 출력의 앞부분과 뒷부분에 1s 가 7개보다 많거나 적은 비트열은 모두 제외할 수 있습니다. 비트열이 유효한지 확인하고, 유효하지 않은 경우 이를 제거하는 함수를 정의합니다. 허위 비트열을 걸러낸 후, 나머지는 대각화 알고리즘으로 전달됩니다. 아래의 PRUNE 플래그를 사용하여 두 가지 동작 간에 전환할 수 있습니다.
가지치기는 회로 수, 진화 시간 집합, 대각항 필터링 등과 함께 최종 부분공간을 형성하는 여러 선택지 중 하나일 뿐이라는 점을 명심하십시오. 전처리된 실행과 전처리되지 않은 실행을 비교하는 것은 다른 모든 조건이 동일하게 유지될 때만 의미가 있습니다. C++ 탭에서는 이 점을 더 자세히 다루고 있는데, 이는 복구(recovery)가 아닌 사후 선택(postselection)을 사용하며 다른 매개변수들에서도 차이가 있기 때문입니다.
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 HaC++ 소스 코드, CMake 프로젝트 및 빌드 지침은 Qiskit 문서 저장소의 docs/tutorials/assets/sqdrift/cpp 디렉터리에 있습니다. FCIDump 입력 파일은 인접한 디렉터리에 fcidump_files 있습니다. 이 튜토리얼을 직접 빌드하고 실행하려면 저장소를 클론하거나 두 디렉터리를 모두 다운로드하세요.
예상 소요 시간: Heron r3 프로세서 기준 180초 (참고: 이는 단지 예상치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)
학습 성과 (C++)
- 트로터화 방식에 비해 더 얕은 회로를 설계하는 방법을 알아보세요
- qDRIFT 와 SQD를 활용한 기저 상태 추정 전체 워크플로우를 살펴봅니다
- 다른 Qiskit 애드온과 함께
qiskit-fermions사용하여 이러한 워크플로를 구현하는 방법을 알아보세요
필수 조건 (C++)
- ‘샘플 기반 양자 대각화(SQD)’ 개요 읽어보기
- '샘플 기반 크릴로프 양자 대각화(SKQD) ' 강의 읽기
배경 (C++)
SqDRIFT 이는 SKQD의 변형으로, 비트스트링을 샘플링할 안자츠(ansatz)를 선택할 필요성을, 대상 해밀토니안으로부터 직접 구성된 시간 진화 회로 집합으로 대체한 것이다. 이는 해밀토니안의 계수를 바탕으로 해밀토니안에서 더 작은 시간 진화 연산자를 하위 표본 추출함으로써 달성되며, 이를 ‘ qDRIFT ’ 트로터화 방법이라고 한다.
두 구현 방식 모두 Qiskit Fermions를 사용하여 분자 해밀토니안을 표현하고, ‘ qDRIFT ’ 알고리즘을 위한 회로를 구성합니다. Python 구현은 페르미온 트랜스파일러 단계를 사용하는 반면, C++ 구현은 그룹을 명시적으로 샘플링하여 큐비트 연산자로 매핑합니다.
해밀토니안이 다음과 같은 형태라고 하자:
여기서, 일반성을 잃지 않고, 를 만족하며, 의 최대 고유값의 절댓값이 와 같다고 가정한다. 부호가 있는 또는 복소수인 모든 선인자는 로 흡수되므로, 계수 는 엄격히 양의 가중치이며, 는 각 항의 방향을 나타낸다. 여기서 는 해밀토니안의 항의 개수(또는, 그룹화 후의 그룹 개수)를 나타내며, 이는 해밀토니안의 속성으로서, 아래에서 로 표기된 단일 회로에 샘플링된 연산자의 개수와는 구별된다.
그런 다음, qDRIFT 알고리즘은 목표 시간 에 대해, 어떤 연산자 를 구현하며, 여기서 는 에서 시작하여 다음과 같이 정의된 SqDRIFT 회로를 나타냅니다:
여기서 는 회로당 표본 추출된 연산자의 수이며, 는 앙상블 내의 회로 수입니다. 이 제품은 의 추출 결과를 기반으로 하며, 의 모든 해밀토니안 항을 대상으로 하는 것은 아닙니다. 또한 항들이 중복 허용 방식으로 추출되므로, 동일한 가 단일 내에서 두 번 이상 나타날 수 있습니다.
수량:
이는 계수들의 노름이므로, 단계 각각은 어떤 항이 추출되었는지에 관계없이 동일한 지속 시간 동안 진행됩니다. 계단 각도의 균일성은 qDRIFT: 의 특징이며, 계수(coefficient)는 해당 항이 회전되는 각도가 아니라, 그 항이 그려지는 빈도를 통해 결과에 영향을 미칩니다. 지수는 다음 분포에서 표본 추출됩니다:
따라서 수열 는 이 분포에서 추출된 항 지수의 무작위 순서열입니다. 가 양수이며 합이 이므로, 이는 정규화된 확률 분포이며, 무작위 추출에 대한 결과 채널의 기대값은 하에서의 진화를 근사하며, 가 커짐에 따라 오차는 줄어든다. 근사 오차는 항의 개수 가 아니라 에 따라 달라진다는 점에 유의하십시오.
( SqDRIFT 의 논문에서는 항의 수를 , 수열의 길이를 로 표기하고 있으나, 여기서는 두 가지를 명확히 구분하기 위해 와 를 사용합니다.)
이 튜토리얼에서는 이러한 무작위 회로들의 앙상블을 생성하는 방법을 설명합니다. 이러한 회로를 생성한 후, 서로 다른 연산자에 대해 크릴로프 부분공간을 생성하는 방식과 유사하게, 서로 다른 시간 매개변수를 갖는 여러 연산자로부터 비트열을 표본 추출합니다. 이를 통해 기저 상태 벡터와 샘플링된 비트열 간의 중첩도가 높아집니다.
요구 사항 (C++)
이 구현은 IBM Quantum® 하드웨어에서 실행되며, 고전적인 대각화 연산을 위해 외부 SBD 실행 파일을 사용합니다. Python 시뮬레이터 예제는 포함되어 있지 않습니다.
C++ 빌드 지침에 따라 컴파일러, CMake, Rust, Boost 및 Qiskit 종속성을 설치하십시오. rustup을 사용하여 Rust를 설치하세요: 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 파일을 ( docs/tutorials/assets/sqdrift/cpp 프로젝트 루트)에 저장하며, 에는 저장하지 않습니다 build/. 프로젝트 루트 디렉터리에서 ./SqDRIFT 실행하여 파일을 찾을 수 있도록 하세요 ../fcidump_files/N2_sto_3g. 이 탭에 있는 다른 모든 명령어도 별도로 명시되지 않는 한 프로젝트 루트에서 실행됩니다. 만약 공유 라이브러리 누락 오류로 인해 실행에 실패할 ./SqDRIFT 경우, 빌드 가이드에 명시된 플랫폼별 라이브러리 경로 지침을 따르십시오. macOS, 에서는 일반적으로 이러한 설정 없이도 실행 파일이 정상적으로 작동합니다.
설정 (C++)
아래의 발췌문은 ‘ SqDRIFT.cpp’ 전체 프로그램에서 발췌한 내용입니다. 해당 파일을 하나의 프로그램으로 빌드하고 실행하십시오.
SqDRIFT, 를 실행하기 전에, IBM Quantum API 토큰이 IBM Cloud 플랫폼 에 맞게 설정되어 있는지 확인하십시오.
Python 가 qiskit-ibm-runtime 설치되어 있다면, 올바른 형식의 설정 파일을 만드는 가장 쉬운 방법은 다음과 같습니다:
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum_platform", token="YOUR_IBM_QUANTUM_TOKEN_HERE")이 코드는 올바른 모양으로 ( Linux/macOS~/.qiskit/qiskit-ibm.json ) 또는 %USERPROFILE%\.qiskit\qiskit-ibm.json (Windows)를 출력합니다. 파일을 수동으로 생성하고 싶다면:
{
"default-ibm-quantum-platform": {
"channel": "ibm_quantum_platform",
"token": "YOUR_IBM_QUANTUM_TOKEN_HERE"
}
}IBM Quantum Platform 에서 API 키를 발급받으세요.
참고: 구성 파일이 없거나 형식이 잘못된 경우, 시작 시 명확한 오류 메시지가 아닌 도움이 되지 않는 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;
출력:
✓ 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);
출력:
Grouped into 1902 groups
진화 회로를 구성하기 전에, 전자 구조에 따라 해밀토니안 항들을 그룹화했다.
샘플링 가중치를 계산한 다음, 정규화하고 큐비트 연산자로 매핑한다
여기서는 순서가 중요합니다. 정규화는 모든 계수의 크기를 그 자리에서 1로 재조정하므로, 먼저 표본 가중치를 계산해야 합니다. 정규화가 먼저 수행된다면, 모든 는 정확히 로 재현되며 1.0 , 각 그룹의 가중치는 해당 항의 단순한 개수로 축소됩니다. 이 경우 표본 분포는 물리적 계수가 아닌 그룹의 크기에 의해 결정될 것입니다.
각 그룹의 가중치는 해당 항들의 평균 절대 계수입니다. Python 탭의 QDriftTrotterization pass는 그룹화된 해밀토니안에 대해 동일한 가중치를 사용합니다. 대신 합을 사용하면 가 그룹 크기에 따라 비례하게 되어, 결과적으로 진화 시간이 왜곡됩니다. .
// 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;
출력:
Total weight (λ): 337.261
Mapped all 1902 normalized groups to qubit operators
가중치 는 원본 계수에서 읽어온 그룹 의 각 항에 대한 평균 절대 계수입니다. 그런 다음 정규화를 통해 조던-위그너 변환을 수행하기 전에 각 페르미온 항의 크기를 1로 재조정하며, 진화된 연산자에서는 해당 항의 위상 및 부호만 보존됩니다. 이는 qDRIFT's 의 정규화된 항 진화와 일치합니다. 를 통해 계수의 크기에 따라 표본 추출 확률이 결정되는 반면, 회로 진화에서는 정규화된 항을 사용하므로 큰 계수가 두 번 계산되지 않습니다.
연산자 집합 예시
// 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);
}
}
출력:
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;
출력:
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에 대한 회로 통계 및 연산자 인덱스를 출력하지만, 여기서는 이를 생략합니다.
샘플링된 각 연산자에 대해, 시간 를 갖는 스즈키-트로터 진화 회로를 생성하며, 여기서 는 회로당 샘플링된 연산자의 수이고, 는 시간 단계입니다. 매핑 전에 각 표본 그룹이 항별로 정규화되었기 때문에, 이 변환은 회전 과정에서 각 항의 위상 및 부호에만 영향을 미치며, 크기는 표본 분포를 통해서만 기여합니다. 하트리-팍(Hartree-Fock) 초기 상태는 큐비트 06과 1016(각 스핀 부문에 7개의 전자)에 X 게이트를 적용하여 준비된다. 각 진화 회로의 명령어는 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 에 연결하여 이름으로 백엔드를 선택하고, 해당 백엔드에 맞춰 각 회로를 트랜스파일링한 뒤, 회로당 하나의 작업을 제출합니다. 즉, 20 큐비트 레지스터에서 각각 100회의 샷으로 구성된 100개의 개별 Sampler 작업을 제출합니다. 그런 다음 각 작업이 완료될 때까지 폴링을 수행하고 에서 비트스트링을 수집합니다 result[0]. 모든 작업은 QPU 시간을 사용하며 대기열에서 각각 대기하므로, 이 단계는 단일 일괄 처리 작업보다 훨씬 더 오래 걸릴 것으로 예상됩니다.
이 페이지에 제시된 하드웨어 결과는 ( ibm_pittsburgh 100개 회로 × 100회 실행) 테스트를 통해 도출된 것으로, 값은 기본값에서 backend_name 변경되었습니다 ibm_fez. 저장된 샘플은 4단계에 표시된 수정된 결정자 비트 순서에 따라 후처리를 거쳤습니다. 하드웨어 노이즈는 실행마다 달라지기 때문에, 카운트 값과 최종 에너지는 달라질 수 있습니다.
출력:
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 in 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;
출력:
✓ 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개의 스핀-다운 전자)를 가진 비트열을 선별하고, 스핀 대칭화를 통해 이를 구성 상호작용(CI) 문자열로 변환한 뒤, 후속 단계인 선택 기저 대각화 처리를 위해 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++은 RAII를 통해 자동으로 정리됩니다 std::vector<QuantumCircuit> . 큐비트 연산자, 페르미온 연산자 및 FCIDump 데이터를 수동으로 해제합니다.
이제 해밀토니안을 투영할 기반을 마련했으므로, 대각화 과정을 진행하여 기저 상태의 근사값을 구할 수 있습니다. 프로젝트 루트 디렉터리에서 다음 명령을 실행하십시오 docs/tutorials/assets/sqdrift/cpp.
SBD 바이너리 diag 빌드하기
CMake는 SBD를 소스 코드 형태로만 가져옵니다( deps/sbd/ 주 빌드 과정에서는 의도적으로 이를 컴파일하지 않습니다). SBD 앱은 일반 를 사용하며 Makefile, SBD 자체는 MPI, OpenMP, 및 BLAS/LAPACK을 필요로 합니다. 따라서 이러한 구성 요소가 설치되고 앱의 Configuration 파일이 사용자의 툴체인과 일치할 때까지는 일반 만으로는 충분하지 않습니다 make .
는 같은 디렉터리에 있는 Configuration 파일에서 컴파일러 및 링크 플래그를 Makefile 읽어옵니다. 업스트림에 배포된 버전은 특정 macOS/Homebrew 레이아웃에 맞춰 조정되어 있어, 기본 설정의 컴퓨터에서는 그대로 빌드할 수 없습니다. 특히 -I/opt/homebrew/opt/llvm/include 가 포함되어 있지 않으며( omp.h 이는 버전별 clang 리소스 디렉터리에 있습니다), Apple의 는 이를 아예 -fopenmp 거부합니다 clang++ :
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: 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-devSBD_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: 데이비슨 대각화법--iteration 100: 데이비슨 반복의 최대 횟수--block 10: 데이비슨 알고리즘의 블록 크기--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 ’ 예제와 어떻게 다른가
이 두 예시는 대칭성이 깨진 비트열을 처리하는 방식뿐만 아니라 여러 측면에서 동시에 차이가 있으므로, 동일한 에너지를 산출하지는 않을 것으로 예상됩니다:
칼럼 1 | C++ 탭 | Python 탭 |
|---|---|---|
| 대칭성이 깨진 비트열 | Hamming weight에 postselect_bitstrings 대해 에 의해 배제됨 | 반복적인 구성 복구를 통해 복구됨 |
| 루프 구조 | 단일 단계: 샘플 → 사후선택 → 한 번 대각화 | 외부 루프: 대각화, 궤도 점유율 읽기, 구성 복원, 재대각화 |
| 회로 수 | 100회 회로 (num_circuits = 100) | 400회 회로 (진화 시간당 200회) |
| 진화 시기 | (time_step = 1) 하나 | 두 시간, times = [1.0, 10.0] |
| 대각항 | 샘플링된 연산자에 보관됨 | 에 의해 제거되어 filter_diagonal_terms, 표본 분포와 회로가 변경되었습니다 |
| 항목 순서 지정 | ‘그룹화됨’만 | canonical_order 재현성을 위해 그룹화 전에 적용됨 |
| 대각화 솔버 | SBD (diag, external MPI/OpenMP binary) | qiskit-addon-sqd 실행 중인 솔버 |
| 백엔드 | 이름을 수정했습니다 (backend_name = "ibm_fez"; 이 페이지에서 실행된 코드에서는 ibm_pittsburgh) | least_busy 충분한 큐비트를 갖춘 운영용 백엔드 |
| 전사 | 각 회로별로, 최적화 수준 2 | 모든 회로를 종합하여, 최적화 수준 3 |
| 작업 제출 | 100개의 잡, 각각 하나의 회로 | 400개의 회로를 모두 포함하는 하나의 샘플러 작업 |
| 시뮬레이터 실행 | 없음; 하드웨어만 | 하드웨어 실행 전의 무음 시뮬레이터 예시 |
두 버전 모두 동일한 해밀토니안을 사용하며, 항들을 전자 구조에 따라 그룹화하고, 각 그룹의 항들에 대해 평균 절대 계수를 가중치로 적용하며, 하트리-팍(Hartree-Fock) 상태에서 시작하고, 회로당 10개의 그룹과 100회의 시뮬레이션을 수행합니다.
각 차이는 표본화된 부분공간, 그리고 결과적으로 에너지에 독립적으로 영향을 미칩니다. 더 큰 앙상블과 두 번째로 더 긴 진화 시간은 모두 Python 예제에서 탐구하는 구성의 범위를 넓혀줍니다. 한 번 이상의 시점에서 샘플링을 수행함으로써 SqDRIFT 는 저에너지 공간에 대해 크릴로프(Krylov)와 유사한 커버리지를 확보하게 됩니다. 대각선 항을 필터링하면 샘플러가 추출할 수 있는 연산자와 그 가중치가 달라지므로, 두 실행은 서로 다른 분포를 샘플링하게 됩니다.
사후 선별은 엄격한 필터링 과정으로, 입자 수가 잘못된 샷은 즉시 제외됩니다. 대부분의 데이터를 제거한 위의 실행 결과, 10,000개의 시뮬레이션 중 1,137개(약 11%)가 (7,7) 해밍 가중치 검사를 통과했으며, 이는 단 49개의 서로 다른 -결정식으로 통합되었습니다. 반면, 구성 복원은 이전 대각선화 과정에서 산출된 평균 궤도 점유율을 활용하여 비트를 반전시키고 이러한 샷을 복구함으로써, 사후 선별 과정에서 버려질 데이터를 재활용합니다. 이러한 피드백으로 인해 이 과정은 본질적으로 반복적입니다. in recover_configurations 함수는 대각화 연산이 완료된 후에야 존재하는 avg_occupancies 인수를 필요로 qiskit-addon-sqd-hpc 합니다.
이 C++ 실행 결과, 49개의 -행렬식의 부분공간과 -107.5109 Ha의 에너지가 산출됩니다. 10개의 궤도에 7개의 전자가 있는 경우의 전체 -결정자 공간은 개의 문자열에 불과하므로, 이 120개를 모두 동일한 이진 diag 코더에 입력하면 정확한 FCI 에너지 -107.6482 Ha를 얻을 수 있다. 따라서 이 실행 결과는 정확한 값보다 약 0.1373 Ha 정도 높으며, 120개의 행렬식 중 49개를 아우르고 있다. SQD는 선택된 부분공간에서 변분적이므로, 더 작은 부분공간은 에너지를 높일 수밖에 없으므로, 불완전한 부분공간이 갭을 가장 직접적으로 나타내는 지표가 된다.
이러한 정확한 FCI 검사는 예제가 작기 때문에 가능한 것일 뿐이며, 이는 ‘ SqDRIFT ’ 워크플로우의 일부가 아닙니다. 또한 SQD가 실제로 필요한 규모의 시스템에 대해서는 이러한 참조 자료가 존재하지 않습니다. 이는 이 실행을 통해 얼마나 많은 공간을 회수했는지를 수치적으로 나타내기 위한 것일 뿐입니다.
Python 예제와의 비교는 통제된 조건에서 이루어진 것이 아니므로, 차이를 단순히 구성 정보 복구 기능의 부재만으로 돌릴 수는 없습니다. 회로 수, 단일 진화 시간, 대각항 필터링, 하드웨어 노이즈 등이 모두 동시에 달라지며, 이 모든 요소가 부분공간을 형성한다는 점에 유의하십시오.
이 격차를 줄이기 위해, 이 튜토리얼을 마친 후 시도해 볼 수 있는 몇 가지 다음 단계는 다음과 같습니다: shots, num_circuits, 또는 을 높이고 ops_per_circuit, 하나 이상의 에서 샘플링하며 time_step, 위에 복구 루프를 추가하는 것입니다 Qiskit::addon::sqd::recover_configurations.
다음 단계
- 페르미온 격자 모델에 대한 샘플 기반 크릴로프 양자 대각화 — 변분적 가설 대신 시간 진화 회로를 사용한 관련 튜토리얼.
- 화학 해밀토니안의 샘플 기반 양자 대각화 — 양자 화학 시뮬레이션을 위한 국소 유니터리 클러스터 Jastrow (LUCJ) 회로 구축 방법에 대한 튜토리얼.
- SqDRIFT 에 실린 논문 — 이 튜토리얼의 기초가 된 문헌입니다. (본 논문에서 다룬 일부 최적화 기법은 현재 개발 중인 단계이며, 이 튜토리얼은 사용된 라이브러리의 발전 상황에 따라 향후 변경될 수 있음을 유의하시기 바랍니다.)