Skip to main content
IBM Quantum Platform

SqDRIFT 기저 상태 추정 알고리즘

‘ Python ’ 노트북을 다운로드하세요

이 튜토리얼을 직접 실행해 보려면 ‘ Python ’ 노트북을 다운로드하세요. 노트북에는 FCIDump 입력 파일이 아직 없는 경우, 해당 파일을 다운로드합니다.

예상 소요 시간: Heron r3 프로세서 기준 180초 (참고: 이는 단지 예상치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)


학습 성과

  • 트로터화 방식에 비해 더 얕은 회로를 설계하는 방법을 알아보세요
  • qDRIFT 와 SQD를 활용한 기저 상태 추정 전체 워크플로우를 살펴봅니다
  • 다른 Qiskit 애드온과 함께 qiskit-fermions 사용하여 이러한 워크플로를 구현하는 방법을 알아보세요

전제조건


배경

SqDRIFT 이는 SKQD의 변형으로, 비트스트링을 샘플링할 안자츠(ansatz)를 선택할 필요성을, 대상 해밀토니안으로부터 직접 구성된 시간 진화 회로 집합으로 대체한 것이다. 이는 해밀토니안의 계수를 바탕으로 해밀토니안에서 더 작은 시간 진화 연산자를 하위 표본 추출함으로써 달성되며, 이를 ‘ qDRIFT ’ 트로터화 방법이라고 한다.

두 구현 방식 모두 Qiskit Fermions를 사용하여 분자 해밀토니안을 표현하고, ‘ qDRIFT ’ 알고리즘을 위한 회로를 구성합니다. Python 구현은 페르미온 트랜스파일러 단계를 사용하는 반면, C++ 구현은 그룹을 명시적으로 샘플링하여 큐비트 연산자로 매핑합니다.

해밀토니안이 다음과 같은 형태라고 하자:

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

여기서, 일반성을 잃지 않고, ci>0c_i > 0 를 만족하며, hih_i 의 최대 고유값의 절댓값이 11 와 같다고 가정한다. 부호가 있는 또는 복소수인 모든 선인자는 hih_i 로 흡수되므로, 계수 cic_i 는 엄격히 양의 가중치이며, hih_i 는 각 항의 방향을 나타낸다. 여기서 NN 는 해밀토니안의 항의 개수(또는, 그룹화 후의 그룹 개수)를 나타내며, 이는 해밀토니안의 속성으로서, 아래에서 nn 로 표기된 단일 회로에 샘플링된 연산자의 개수와는 구별된다.

그런 다음, qDRIFT 알고리즘은 목표 시간 tt 에 대해, 어떤 연산자 VkV_k 를 구현하며, 여기서 kk 는 1⋯K1 \cdots K 에서 시작하여 다음과 같이 정의된 kthk_{th} SqDRIFT 회로를 나타냅니다:

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

여기서 nn 는 회로당 표본 추출된 연산자의 수이며, KK 는 앙상블 내의 회로 수입니다. 이 제품은 nn 의 추출 결과를 기반으로 하며, NN 의 모든 해밀토니안 항을 대상으로 하는 것은 아닙니다. 또한 항들이 중복 허용 방식으로 추출되므로, 동일한 hih_i 가 단일 VkV_k 내에서 두 번 이상 나타날 수 있습니다.

수량:

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

이는 계수들의 L1L_1 노름이므로, nn 단계 각각은 어떤 항이 추출되었는지에 관계없이 동일한 지속 시간 λt/n\lambda t / n 동안 진행됩니다. 계단 각도의 균일성은 qDRIFT: 의 특징이며, 계수(coefficient)는 해당 항이 회전되는 각도가 아니라, 그 항이 그려지는 빈도를 통해 결과에 영향을 미칩니다. 지수는 다음 분포에서 표본 추출됩니다:

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

따라서 수열 (k1,…,kn)(k_1, \ldots, k_n) 는 이 분포에서 추출된 항 지수의 무작위 순서열입니다. cic_i 가 양수이며 합이 λ\lambda 이므로, 이는 정규화된 확률 분포이며, 무작위 추출에 대한 결과 채널의 기대값은 HH 하에서의 진화를 근사하며, nn 가 커짐에 따라 오차는 줄어든다. 근사 오차는 항의 개수 NN 가 아니라 λ\lambda 에 따라 달라진다는 점에 유의하십시오.

( SqDRIFT 의 논문에서는 항의 수를 N\mathcal{N}, 수열의 길이를 NN 로 표기하고 있으나, 여기서는 두 가지를 명확히 구분하기 위해 NN 와 nn 를 사용합니다.)

이 튜토리얼에서는 이러한 무작위 회로들의 앙상블을 생성하는 방법을 설명합니다. 이러한 회로를 생성한 후, 서로 다른 연산자에 대해 크릴로프 부분공간을 생성하는 방식과 유사하게, 서로 다른 시간 매개변수를 갖는 여러 연산자로부터 비트열을 표본 추출합니다. 이를 통해 기저 상태 벡터와 샘플링된 비트열 간의 중첩도가 높아집니다.


요구사항

이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하십시오

  • Python (>= 3.10 ) 가상 환경
  • pip>=25.1
  • qiskit ~= 2.5
  • qiskit-fermions== 0.1.0 (이름이 복수형이라는 점에 유의하세요)
  • numpy
  • pyscf
  • qiskit-aer
  • qiskit-ibm-runtime
  • qiskit-addon-sqd

다음 명령어를 사용하여 필요한 모든 패키지를 설치할 수 있습니다:

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

설정

# 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 A˚\AA 인 질소 분자( N2N_2 )를 N2_sto_3g 설명하며, 이는 실험적으로 확인된 평형 결합 길이입니다. 이 방정식의 헤더에는 NORB=10, NELEC=14, 및 가 선언되어 있습니다 MS2=0: 10개의 공간 궤도(따라서 20개의 스핀 궤도, 그리고 조던-위그너 표기법 하에서 20개의 큐비트), 스핀 싱글렛에 속한 14개의 전자, 즉 7개의 α\alpha 전자와 7개의 β\beta 전자입니다. 모든 궤도에 대칭 표식 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 가정한다는 점에 유의하십시오.

대각선 항 필터링

회로를 생성하는 데 사용되는 해밀토니안에서 대각선 항들을 제거함으로써, nn qDRIFT 샘플링 슬롯이 구성 간에 인구(population)를 이동시키는 항들에 사용되도록 합니다. 이러한 항들은 다음 단계에서 Evolution 게이트가 구성되기 전인 이 시점에서 해밀토니안에서 제거하는 것이 가장 좋습니다.

문제의 항들은 직업-수 기저에서 대각선에 위치한 항들, 즉 수 연산자 ai†aia^\dagger_i a_i 의 곱들입니다. 이 정의에 해당하는 항은 세 가지 종류가 있습니다:

  • 상수 에너지 오프셋, 즉 0차 수 연산자의 곱으로, 그 시간 진화는 전체 위상에만 기여하며;
  • nin_i 와 같은 개별 수 연산자들로, 이들의 시간적 진화는 단일 큐비트 ZZ 회전으로 환원되며;
  • ninjn_i n_j 와 같은 고차 곱.

이들 중 어느 것도 그 자체로는 직업-숫자 구성 간에 인구를 이동시키지 않으며, 이미 존재하는 구성의 단계에만 영향을 미칩니다. 그러나 이것들이 단순히 무의미한 것은 아닙니다. 이러한 상대 위상들은 회로 후반부의 여기 항에 의해 생성되는 간섭에 영향을 미치므로, 이를 필터링하면 실제로 생성되는 진화 과정이 바뀌게 되고, 샘플링 분포도 달라질 수 있습니다. 이는 회로 생성 단계에서 샘플링을 여기 항에 집중시키기 위해 의도적으로 적용된 근사법일 뿐, 샘플링된 분포를 그대로 두는 단계는 아닙니다. 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개의 α\alpha 전자와 7개의 β\beta 전자를 나타냅니다.

# 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차 이상의 항이 포함되어 있더라도 하드웨어에서 더 효율적으로 실행될 수 있는 더 단순한 회로를 만들 수 있게 되었습니다. 기간별 그룹화 후, 가중치를 기준으로 연산자를 표본 추출합니다. 각 연산자 hih_i 에 대해, 가중치 WhiW_{h_i} 는 다음과 같이 정의된다:

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

1단계에서 항들이 그룹으로 묶였기 때문에, 여기에서 각 hih_i 는 하나의 전체 그룹을 나타냅니다. 즉, cic_i 는 그룹 ii 에 속한 항들의 평균 절대 계수이며, 이 그룹에 속한 각 항은 계수가 부호로만 축소된 형태로 표현됩니다.

페르미온 최적화 및 하드웨어 네이티브 최적화

이 함수는 를 입력으로 받아 FermionicCircuit , 하드웨어에서 실행되도록 트랜스파일링할 수 있는 최적화된 최종 회로를 생성하는 MultiStagePassManager 를 반환합니다 generate_preset_jw_pass_manager() . 기본 최적화 단계를, 우리의 패스가 QDriftTrotterization 포함된 FermionicPassManager 단계로 대체합니다:

  • 이 QDriftTrotterization 패스는 내부적으로 가중치 계산 및 샘플링 기능을 활용하여, 샘플링에 사용할 회로를 생성합니다
  • 이 RelabelModes 패스는 페르미온 모드를 재배열하여 큐비트 간의 연결성을 최적화하고 게이트 깊이를 줄일 수 있는 또 다른 최적화 단계입니다. 자세한 내용은 API 참조 문서를 참조하십시오

나머지 단계들은 자동으로 MultiStagePassManager 실행되며, 페르미온에서 큐비트로의 전체 매핑을 처리합니다:

  • F2QLayout : 프리셋 패스 관리자는 해당 패스를 TrivialF2QLayout 적용하며, 이는 nn 의 페르미온 비트를 nn 의 큐비트로 간단히 매핑합니다.
  • 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 processing

4단계: 후처리를 수행하고 원하는 기존 형식으로 결과를 반환합니다

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를 실행하기 전에 유효하지 않은 비트스트링을 걸러낼 수도 있고, 그렇지 않고 바로 다음 단계로 진행할 수도 있습니다. 하드웨어 실행의 경우 일반적으로 가지치기를 생략하는 것이 바람직합니다. 이는 대칭성이 깨진 샷을 구성 복구 과정에서 활용할 수 있게 해주기 때문인데, 이를 통해 해당 샷을 유효한 구성으로 복구하여 해당 샷을 아예 버리는 대신 부분공간을 확장할 수 있기 때문입니다.

질소는 α\alpha 와 β\beta 전자를 각각 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 Ha

다음 단계

권장사항
이 페이지가 도움이 되었습니까?
GitHub에서 버그, 오타를 보고하거나 컨텐츠를 요청하십시오.