주 콘텐츠로 건너뛰기
이 프로젝트는 베타 단계입니다. 버그를 발견하셨나요? GitHub 이슈 만들기. 질문이나 아이디어가 있으신가요? Discussions에서 질문하기 — 피드백을 기다립니다!
이 페이지의 번역 품질은 어떻습니까?

Slater 행렬식 준비 최적화

중요

이 가이드의 개념은 현재 Python API에서만 사용할 수 있습니다. 동등한 기능은 향후 릴리스에서 C API에도 제공될 예정입니다.

InitializeModes 게이트는 기준 모드 점유를 선언하고, OrbitalRotation 게이트는 단일 입자 기저를 회전시킵니다. 두 게이트를 연달아 배치하면 Slater 행렬식이 준비됩니다. 모드는 기준 행렬식에서 시작한 뒤 목표 오비탈로 회전됩니다. PrepareSlaterDeterminant 게이트는 이 합성을 하나로 담으며, 기준 점유를 가지고 있기 때문에 트랜스파일할 때 OrbitalRotation 단독으로 필요한 전체 정사각 분해 대신, 점유된 오비탈만 구현하는 직사각형 givens_decomposition_slater()를 사용할 수 있습니다. 이는 엄밀히 더 저렴한 합성입니다(이 가이드 끝부분의 이점 참조).

MergeSlaterDeterminantPreparation Transpiler 패스는 FermionicCircuit에서 InitializeModes-다음-OrbitalRotation 패턴을 감지해 PrepareSlaterDeterminant 게이트로 다시 작성하므로, 이후 합성 단계에서 축소된 분해가 자동으로 적용됩니다. 시뮬레이션에서 이 재작성은 상태에 아무 영향을 주지 않습니다. PrepareSlaterDeterminant는 검증 후 회전 방식이므로(두 개의 별도 게이트와 마찬가지로 기준 점유를 검증한 다음 회전을 적용합니다), 병합은 준비되는 상태를 바꾸지 않으면서 더 저렴한 합성을 가능하게 합니다.

이 가이드 전반에서 각 회로는 패스 적용 전후를 나란히 그립니다. 왼쪽이 입력이고 오른쪽이 패스를 실행한 결과입니다. 두 개의 도우미 함수가 이를 수행합니다. merge는 회로의 복사본에 패스를 실행하고(FermionicDAGCircuit를 통해) 결과를 그릴 수 있는 FermionicCircuit으로 다시 변환하며, draw_merge는 두 부분을 하나의 전/후 그림으로 그립니다.

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig

병합되는 대상​

이 패스는 세 가지 패턴을 인식하며, 모두 InitializeModes와 OrbitalRotation이 사용하는 블록 스핀 모드 규약을 기반으로 합니다. norb개의 공간 오비탈이 있는 시스템에서 모드 0..norb는 스핀 알파 섹터이고 norb..2*norb는 스핀 베타 섹터입니다. 각 섹터는 고유한 (전자 수, 오비탈 수) 형태를 가지므로 Slater 행렬식 준비는 스핀 섹터별로 수행됩니다.

전체 레지스터(또는 스핀 없음)​

가장 단순한 패턴은 InitializeModes 바로 뒤에 같은 모드에 대한 OrbitalRotation이 오는 경우입니다. 이는 스핀 없는 경우(모든 모드에 대한 하나의 행렬식)이면서, 회전이 두 섹터에 한꺼번에 걸치는 스핀 있는 회로이기도 합니다. 두 게이트는 하나의 PrepareSlaterDeterminant로 융합됩니다.

>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A mode initialization and rotation fusing into one Slater determinant preparation.

스핀 섹터별​

형태는 같지만 한쪽 스핀 절반으로 제한됩니다. 여기서는 섹터마다 하나씩, 서로 독립적인 초기화-후-회전 쌍 두 개가 각각 따로 PrepareSlaterDeterminant로 융합됩니다.

>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

Two per-sector initialize-and-rotate pairs, each fusing into its own preparation.

전역 초기화, 스핀별 회전​

이는 단일 전체 레지스터(2 * norb) InitializeModes 뒤에 연속된 각 스핀 절반에 하나씩, 두 개의 OrbitalRotation이 오는 경우입니다. 패스는 전역 점유를 섹터 경계에서 분할하여 두 개의 PrepareSlaterDeterminant 게이트를 생성합니다. 두 회전은 어느 순서로 나타나도 됩니다.

이것은 국소 유니터리 클러스터 Jastrow(LUCJ) 워크플로가 만드는 형태입니다(LUCJ 가이드 참조). 사용자는 회로 맨 앞에 InitializeModes(일반적으로 from_hartree_fock()를 사용한 Hartree-Fock 기준)을 배치하고 UCJ 앤서츠를 덧붙이며, 앤서츠를 분해하면 처음 두 개의 스핀별 오비탈 회전이 초기화 바로 뒤에 옵니다.

>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with two per-spin rotations fusing into two preparations.

이 패턴은 초기화 바로 뒤의 게이트가 스핀 절반 하나만 회전하는 경우에도 적용됩니다. 한쪽 스핀 섹터의 오비탈 회전만 회로 맨 앞에 오는 경우 이런 형태가 만들어집니다. 회전된 절반은 해당 회전과 함께 PrepareSlaterDeterminant가 되고, 나머지 절반은 항등 회전으로 준비됩니다. 항등 준비는 InitializeModes가 그 절반에 대해 어차피 내보냈을 기준 X 게이트로 합성되므로, 채워 넣어도 추가 게이트 비용이 들지 않으면서 회전된 절반에는 축소된 Slater 합성이 적용됩니다.

>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with a single per-spin rotation fusing into two preparations.

변경되지 않는 대상​

이 패스는 의도적으로 보수적입니다. 두 게이트가 실제로 Slater 행렬식 준비로 합성될 때만 융합하고, 나머지는 모두 그대로 복사합니다. “바로 뒤”는 회로의 데이터 흐름 그래프를 기준으로 해석됩니다. OrbitalRotation은 InitializeModes가 모든 모드에 걸쳐 그 유일한 선행자일 때만 융합됩니다. 아래 그림에서는 전과 후가 동일합니다. 아무것도 융합되지 않았습니다.

사이에 있는 연산​

초기화와 회전 사이의 모드에 어떤 게이트라도 작용하면 더 이상 단순한 준비로 합성되지 않으므로 아무것도 병합되지 않습니다.

>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

An operation between the initialization and rotation blocks the merge.

스핀 절반이 아닌 부분 회전​

전체 레지스터 초기화의 연속된 스핀 절반 하나를 정확히 덮는 회전은 융합됩니다(이전 섹션 참조). 그러나 그 밖의 부분 모드 집합(초기화보다 모드가 적고 두 스핀 절반 중 어느 것도 아닌 경우, 예를 들어 섹터 경계에 걸친 하위 범위)에 대한 회전은 섹터별 준비가 아닙니다. 이를 병합하면 회전이 빠뜨린 모드에서 초기화의 역할이 조용히 사라지므로, 패스는 두 게이트를 그대로 둡니다.

>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A partial rotation that is not a spin half is left unmerged.

중요한 이유: 합성의 이점​

PrepareSlaterDeterminant는 흡수하는 OrbitalRotation보다 훨씬 적은 게이트로 합성되기 때문에 병합할 가치가 있습니다. nn개 오비탈에 대한 mm전자 행렬식은 최대 m(n−m)m(n-m)개의 2큐비트 회전이 필요하고 대각 위상 게이트는 필요 없으며, 전체 정사각 오비탈 회전은 n(n−1)/2n(n-1)/2개의 회전에 더해 nn개의 위상이 필요합니다.

프리셋 Jordan-Wigner 패스 매니저는 최적화 단계에서 MergeSlaterDeterminantPreparation을 실행하므로 이 감소는 자동으로 일어납니다. 2전자, 6오비탈 준비를 QuantumCircuit까지 완전히 트랜스파일하고 게이트 수를 세어 보세요.

참고

프리셋은 MergeSlaterDeterminantPreparation 바로 앞에서 MergeOrbitalRotations를 실행합니다. 이는 초기화 뒤에 연속된 OrbitalRotation 게이트의 연속열이 올 때 중요합니다. 앞선 패스가 먼저 연속열을 하나의 회전으로 합치고, MergeSlaterDeterminantPreparation이 이를 초기화와 묶어 하나의 PrepareSlaterDeterminant로 만듭니다. 이 순서가 없으면 연속열의 첫 번째 회전만 흡수되고 나머지는 전체 정사각 분해로 합성됩니다.

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized merged Slater determinant preparation.

같은 오비탈 회전을 단독으로 합성하는 경우와 비교해 보세요. 앞선 초기화가 없으면 병합할 것이 없으므로, 프리셋은 더 깊고 대각 위상 게이트를 포함하는 전체 정사각 분해로 되돌아갑니다.

>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized bare orbital rotation, with its extra phase gates.

축소된 분해는 여섯 개의 위상 게이트를 모두 없애고 2큐비트 회전 수를 거의 절반으로 줄입니다. 준비된 행렬식은 전역 위상(상태 준비에서는 물리적으로 무관함)을 제외하면 올바르며, 이는 위상 게이트가 보정해 주었을 부분입니다.

게이트를 분해하는 두 가지 방법​

PrepareSlaterDeterminant는 두 가지 다른 방식으로 분해할 수 있으며, 둘은 서로 바꿔 쓸 수 없습니다.

decompose()​

게이트를 그 정의로 확장합니다. 기준의 InitializeModes 뒤에 전체 OrbitalRotation이 이어집니다. 이는 어떤 입력 상태에 대해서도 게이트를 연산자로서 동등하게 나타냅니다.

PrepareSlaterDeterminant.synthesis, F2FSynthesis가 적용​

게이트를 이 가이드의 축소된 분해로 대체합니다. 이는 상태 준비로서만 게이트와 동등합니다. 모드가 진공에서 시작할 때 전역 위상을 제외하고 동일한 행렬식을 만들어 냅니다. 바로 그것을 가정하기 때문에 더 저렴합니다.

게이트가 진공에서 상태를 준비할 때, 즉 게이트의 본래 용도이자 트랜스파일러가 알아서 해 주는 경우에는 합성을 사용하세요. 다른 게이트가 같은 모드에서 앞에 오는 등 게이트가 계속 연산자로 작용해야 하거나, 단순한 게이트를 검사하거나 시뮬레이션하려는 경우에는 decompose()를 사용하세요. F2FSynthesis 패스는 어느 경우를 보고 있는지 알 수 없으므로, 진공에서 행렬식을 준비하도록 회로를 구성하는 것은 사용자의 몫입니다.

ffsim으로 회로를 시뮬레이션할 때는 둘 다 필요하지 않습니다. 모든 게이트가 전체 유니터리를 적용하므로 PrepareSlaterDeterminant를 포함한 회로는 있는 그대로 시뮬레이션되며, 그 주변의 OrbitalRotation 게이트에 대한 빠른 경로도 유지됩니다. circuit.decompose("PrepareSlaterDeterminant")로 Slater 게이트만 분해하는 경우에도 그 회전들은 온전하게 남습니다.

함께 보기

PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition, 그리고 전역 초기화 패턴을 만드는 워크플로를 설명하는 LUCJ 가이드를 참조하세요.