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

연산자 표현 매핑하기

매퍼는 BosonOperator를 큐비트 연산자, 여기서는 SparseObservable로 바꿉니다. 연산자 자체는 큐비트에 대해 아무것도 모릅니다. 연산자는 모드에 대한 사다리 연산자를 저장하며, 보손 모드는 임의로 많은 입자를 담을 수 있습니다. 연산자를 유한하게 만드는 모든 것(점유 준위를 몇 개까지 유지할지, 그리고 그 준위들을 큐비트에 어떻게 배치할지)은 매핑할 때 결정됩니다.

라이브러리 매퍼 occupation_encoding()는 점유(Fock) 기저에서 동작합니다. 모드의 준위 nn은 보손이 nn개인 상태입니다. 이 매퍼는 연산자와, 절단과 인코딩을 담는 LevelSpec을 받습니다. 이 가이드는 그 두 가지 선택이 무엇을 뜻하며 각각 어떤 비용이 드는지 설명합니다. 이를 사용하는 전체 과정은 인코딩을 선택하여 Bose-Hubbard 동역학 시뮬레이션하기를, 트랜스파일러가 같은 선택을 회로 전체에 적용하는 방식은 보손 회로 트랜스파일하기를 참고하세요.

Fock 공간 절단하기​

d=nmax+1d = n_\text{max} + 1개의 준위로 유지되는 모드(여기서 nmaxn_\text{max}는 그 max_occupation)는 더 이상 정준 교환 관계를 따르지 않습니다. 그 준위들로 절단된 소멸 연산자를 bˉ\bar{b}라고 쓰면,

[bˉ,bˉ†]=1−d ∣d−1⟩⟨d−1∣ ,[\bar{b}, \bar{b}^\dagger] = 1 - d \, \lvert d-1 \rangle\langle d-1 \rvert \, ,

따라서 이 관계는 가장 높은 준위를 제외한 모든 준위에서 성립합니다 [1]. 두 사다리 연산자의 교환자를 매핑하고 한 모드의 준위들에서 그 대각 성분을 읽으면 이를 확인할 수 있습니다. 다음 도우미 함수는 하나의 모드에 대해 매핑된 연산자의 밀집 행렬을 만드는데, 이는 아주 작은 레지스터에서만 합리적입니다.

>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def level_diagonal(operator, max_occupation, encoding):
... """The diagonal of a mapped one-mode operator on the codewords of the levels."""
... spec = LevelSpec(max_occupation, encoding)
... matrix = SparsePauliOp.from_sparse_observable(occupation_encoding(operator, spec)).to_matrix()
... codewords = [spec.encoding.level_to_bits(level) for level in range(max_occupation + 1)]
... return [round(float(matrix[c, c].real), 6) for c in codewords]
>>>
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dagger = BosonOperator.from_dict({(cre(0),): 1.0})
>>> commutator = (b @ b_dagger) - (b_dagger @ b)
>>> level_diagonal(commutator, 2, "unary_unprojected") # d = 3
[1.0, 1.0, -2.0]

마지막 항목은 1−d1 - d입니다. 이는 {bˉ†,bˉ}\{\bar{b}^\dagger, \bar{b}\}의 생성 공간이 교환자에 대해 닫혀 있지 않다는 뜻이므로, 어떤 인코딩도 대수의 동형 사상이 아닙니다. 인코딩은 상태를, 절단된 부분공간 안에서 작용하는 연산자와 함께 매핑합니다. 다음 두 가지 결과가 실제로 중요합니다.

절단 한계는 도달 가능한 점유수를 한정해야 합니다. 처음 시작하는 점유수만 한정하는 것으로는 충분하지 않습니다. ∣1,1⟩\lvert 1, 1 \rangle에 작용하는 빔 스플리터는 ∣2,0⟩\lvert 2, 0 \rangle과 ∣0,2⟩\lvert 0, 2 \rangle를 채우므로, 절단 한계가 1이면 아무 경고 없이 잘못된 상태가 나옵니다.

>>> from scipy.linalg import expm
>>>
>>> beamsplitter = BosonOperator.from_dict({(cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0})
>>>
>>> def stays_in_11(max_occupation):
... """Probability to remain in |1, 1> after a 50:50 beamsplitter."""
... spec = LevelSpec(max_occupation, "gray").resolve(2)
... observable = occupation_encoding(beamsplitter, spec, spec.total_width)
... matrix = SparsePauliOp.from_sparse_observable(observable).to_matrix()
... unitary = expm(-1j * (np.pi / 4) * matrix)
... index = sum(spec.encoding.level_to_bits(1) << offset for offset in spec.offsets)
... return round(float(abs(unitary[index, index]) ** 2), 6)
>>>
>>> stays_in_11(1), stays_in_11(2)
(1.0, 0.0)

절단 한계가 1이면, 동역학이 채울 두 상태가 존재하지 않기 때문에 ∣1,1⟩\lvert 1, 1 \rangle에 머무를 확률이 1이 됩니다. 절단 한계가 2이면 그 확률은 사라지며, 이것이 Hong-Ou-Mandel 효과입니다. 이 때문에 max_occupation은 기본값이 없는 필수 인수입니다. 이는 모델링 결정이자, 직접 값을 바꿔 가며 확인해야 하는 수렴 매개변수입니다.

매핑은 정규 순서화와 교환되지 않습니다. 연산자 bb†b b^\dagger와 b†b+1b^\dagger b + 1은 절단 전에는 같지만, 서로 다른 큐비트 연산자로 매핑됩니다.

>>> product = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> ordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) + BosonOperator.one()
>>> bool(product.equiv(ordered))
True
>>> level_diagonal(product, 3, "unary_unprojected") # d = 4
[1.0, 2.0, 3.0, 0.0]
>>> level_diagonal(ordered, 3, "unary_unprojected")
[1.0, 2.0, 3.0, 4.0]

두 연산자는 가장 높은 준위를 제외한 모든 준위에서 일치합니다. 최상위 준위에서는 b†b^\dagger가 갈 곳이 없어 진폭이 잘려 나가므로 bb†b b^\dagger는 거기서 0인 반면, b†b+1b^\dagger b + 1은 dd를 줍니다. 해밀토니안이 정규 순서 형태로 정의되는지 한 번 정하고, 그 형태를 일관되게 매핑하세요.

절단 한계를 고르는 데 도움이 되는 세 가지 방법이 있으며, 모두 하한입니다. 연산자는 단일 항 안에서 각 모드에 적용하는 생성 연산자의 최대 개수를 알려 주고, 스펙은 이를 자신의 절단 한계와 비교합니다.

>>> quartic = BosonOperator.from_dict({(cre(0), cre(0), ann(0), ann(0)): 1.0})
>>> quartic.mode_occupations(), quartic.max_occupation()
([2], 2)
>>> LevelSpec(1, "gray").truncation(quartic) # one level short
[1]
>>> LevelSpec(2, "gray").truncation(quartic)
[0]

0이 아닌 항목은 절단 한계가 하한보다 낮다는 뜻이며, 그 값은 얼마나 모자란지를 나타냅니다. 이 도우미 함수들은 동역학을 알지 못하므로, 검사를 통과했다고 해서 절단 한계가 충분하다고 할 수는 없습니다. 위의 빔 스플리터는 하한이 1이지만 절단 한계는 2가 필요합니다. 또한 유지된 준위에서 0이 되는 항에 대해서도 부족분을 보고할 수 있는데, 절단 한계가 1일 때의 b†b†bbb^\dagger b^\dagger b b가 그런 예입니다.

LevelSpec으로 절단 기술하기​

LevelSpec은 모든 모드의 절단 한계를 인코딩과 함께 묶으며, 이로부터 각 모드가 차지하는 큐비트 수와 그 큐비트가 시작되는 위치를 도출합니다. 이에 관해 일치해야 하는 모든 것(매퍼, 트랜스파일러 레이아웃, 상태 준비)에는 동일한 스펙이 주어집니다. 인코딩은 필수 인수이며 절단 한계도 마찬가지로, 둘 다 기본값이 없습니다.

절단 한계는 모든 모드가 공유하는 정수 하나이거나, 모드마다 하나씩의 정수입니다.

>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)

단일 정수로 만든 스펙은 모드 수를 열어 두므로, 너비를 요청하기 전에 이를 확정해야 합니다. 시퀀스는 길이와 관계없이 절대 브로드캐스트되지 않으며, 모드 수를 확정합니다.

>>> per_mode.widths, per_mode.offsets, per_mode.total_width
([3, 2, 2], [0, 3, 5], 7)
>>> shared.resolve(3).widths
[2, 2, 2]
>>> try:
... shared.widths
... except ValueError as error:
... print(error)
this spec was built from a single cutoff, so its number of modes is open; call `resolve(num_modes)` first

모드 mm은 큐비트 offsets[m]부터 offsets[m] + widths[m] - 1까지를 차지하므로, 절단 한계가 서로 다른 모드는 서로 다른 수의 큐비트를 사용할 수 있고, 오프셋은 너비의 누적합입니다. 매퍼에 모드 수를 전달할 필요는 없습니다. 매퍼는 이를 스펙에서 가져오거나, 절단 한계가 하나인 스펙의 경우 min_num_modes()를 통해 연산자에서 가져옵니다. 레지스터는 모드에 필요한 것보다 넓을 수 있으며, 남는 큐비트는 사용되지 않고 끝에 놓입니다.

>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> number.min_num_modes()
1
>>> occupation_encoding(number, LevelSpec(1, "gray")).num_qubits
1
>>> occupation_encoding(number, LevelSpec(1, "gray"), 4).num_qubits
4

모드별 스펙의 마지막 모드를 넘어서는 모드에 작용하는 연산자는 거부됩니다.

>>> far = BosonOperator.from_dict({(cre(5), ann(5)): 1.0})
>>> try:
... occupation_encoding(far, LevelSpec([1, 1], "gray"))
... except ValueError as error:
... print(error)
the operator acts on 6 modes, but the spec covers only 2

인코딩이 큐비트를 세는 방식에서 몇 가지 제한이 따라옵니다. 압축 인코딩에서는 준위가 하나뿐인 모드(max_occupation = 0)가 큐비트를 전혀 사용하지 않는데, 그 하나의 준위는 다른 준위와 구분하기 위해 아무것도 필요하지 않기 때문입니다. 이런 모드는 허용되며 그 뒤에 오는 모드의 큐비트를 옮기지 않지만, 모든 모드가 큐비트를 하나도 사용하지 않게 되는 스펙은 거부됩니다. 유나리 인코딩은 모드당 최대 128개의 준위를 담을 수 있으며, 더 큰 절단 한계는 압축 인코딩을 권하는 오류를 발생시킵니다.

>>> LevelSpec([0, 1], "gray").resolve(2).widths
[0, 1]
>>> try:
... LevelSpec(0, "gray")
... except ValueError as error:
... print(str(error).split(":")[0])
the spec would span no qubits
>>> try:
... LevelSpec(128, "unary_projected")
... except ValueError as error:
... print(str(error).split(" (")[0])
max_occupation = 128 is too large for the unary encodings, which hold at most 128 levels per mode

인코딩 선택하기​

LevelEncoding은 큐비트 블록의 어떤 기저 상태가 각 준위를 나타낼지 결정합니다. 인코딩은 네 가지가 있으며, 일반적으로 어느 하나가 가장 좋다고 할 수는 없습니다 [2]. 모드당 큐비트 수와 각 준위의 비트 패턴은 인코딩에 따라 정해집니다.

>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> rows = ["encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns"]
>>> for encoding in encodings:
... widths = [encoding.num_qubits(levels) for levels in (1, 2, 3, 4, 5, 8)]
... patterns = [f"{encoding.level_to_bits(level):05b}" for level in range(5)]
... rows.append(f"{encoding!s:18s} {widths!s:34s} {patterns}")
>>> print("\n".join(rows))
encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns
unary_projected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
unary_unprojected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
standard_binary [0, 1, 2, 2, 3, 3] ['00000', '00001', '00010', '00011', '00100']
gray [0, 1, 2, 2, 3, 3] ['00000', '00001', '00011', '00010', '00110']

비트 패턴은 비트 qq가 큐비트 qq의 값인 정수입니다. 유나리 인코딩은 준위마다 큐비트 하나를 쓰고 그중 하나를 설정합니다. 압축 인코딩은 ⌈log⁡2d⌉\lceil \log_2 d \rceil개의 큐비트를 쓰며 준위 번호를 이진수로, 또는 이웃한 준위가 한 비트만 다른 Gray 코드로 저장합니다. 인코딩의 문자열 이름은 인코딩이 들어갈 수 있는 어느 곳에서나 허용됩니다.

트레이드오프는 큐비트 수 대 연산자 가중치입니다. 압축 인코딩은 지수적으로 적은 큐비트가 필요하지만, 사다리 연산자는 패턴이 여러 비트에서 다를 수 있는 준위들을 연결하므로 그 항이 더 많은 큐비트에 작용합니다. 이어지는 절에서는 그 비용을 살펴보며, 레이아웃은 같고 연산자만 다른 두 유나리 인코딩부터 시작합니다.

두 유나리 인코딩 비교하기​

유나리 인코딩에서 사다리 연산자는 이웃한 두 큐비트 사이로 들뜸 하나를 옮깁니다. 이를 모드 전체에 대한 연산자로 쓰려면 나머지 큐비트를 어떻게 할지 정해야 합니다. 두 변형은 서로 다르게 선택합니다.

  • unary_projected는 모드의 다른 모든 큐비트에 사영 연산자 ∣0⟩⟨0∣\lvert 0 \rangle\langle 0 \rvert를 놓습니다. 이는 연산자 자체에서 원-핫 제약을 강제합니다. 즉 모드의 나머지가 비어 있을 때만 항이 작용합니다.

  • unary_unprojected는 그 큐비트들을 그대로 두므로, 항은 그 큐비트들에 항등 연산으로 작용합니다.

d=4d = 4에서 단일 b†b^\dagger를 두 방식으로 살펴보세요. 매퍼는 단일 큐비트 비트 항의 문자열로 이루어진 항을 가진 SparseObservable을 반환합니다. 다음은 각각의 처음 네 개 항과, 그 항이 작용하는 큐비트입니다.

>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(b_dagger, LevelSpec(3, encoding))
... terms = observable.to_sparse_list()
... print(encoding, "->", len(terms), "terms on", observable.num_qubits, "qubits")
... for label, qubits, coefficient in terms[:4]:
... print(f" {label:5s} on qubits {qubits} coefficient {complex(coefficient):.4f}")
unary_projected -> 12 terms on 4 qubits
XX00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
XY00 on qubits [0, 1, 2, 3] coefficient 0.0000-0.2500j
YX00 on qubits [0, 1, 2, 3] coefficient 0.0000+0.2500j
YY00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
unary_unprojected -> 12 terms on 4 qubits
XX on qubits [0, 1] coefficient 0.2500+0.0000j
XY on qubits [0, 1] coefficient 0.0000-0.2500j
YX on qubits [0, 1] coefficient 0.0000+0.2500j
YY on qubits [0, 1] coefficient 0.2500+0.0000j

둘 다 같은 계수를 가진 항이 열두 개입니다. 사영된 항은 사영되지 않은 항이 언급하지 않는 두 큐비트에 0 비트 항을 가지므로, 사영된 항은 각각 네 큐비트 모두에 작용하고 사영되지 않은 항은 각각 두 큐비트에 작용합니다. 일반적으로 사영된 항은 모드의 dd개 큐비트 모두에 작용하는데, 각각 XX 또는 YY인 두 개의 인자와 나머지 모든 큐비트의 사영 연산자를 가지기 때문입니다.

두 연산자는 인코딩이 의도한 상태, 즉 원-핫 기저 상태(코드워드)에서는 같고 그 밖에서는 다릅니다. 확인하려면 d=3d = 3에서 두 모드에 대한 작은 연산자를 잡고 아홉 개의 코드워드에서 행렬을 비교하세요.

>>> hamiltonian = BosonOperator.from_dict({
... (cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0,
... (cre(0), ann(0)): 0.5, (cre(1), cre(1), ann(1), ann(1)): 0.25,
... })
>>> matrices = {}
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(hamiltonian, LevelSpec(2, encoding))
... matrices[encoding] = SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>> unary = LevelEncoding.UNARY_PROJECTED
>>> codewords = sorted(
... unary.level_to_bits(first) | (unary.level_to_bits(second) << 3)
... for first in range(3) for second in range(3)
... )
>>> projected, unprojected = matrices["unary_projected"], matrices["unary_unprojected"]
>>> bool(np.allclose(projected[np.ix_(codewords, codewords)], unprojected[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(projected, unprojected))
False

둘 다 코드워드 부분공간을 불변으로 유지하므로, 코드워드에서 준비된 상태는 코드워드에 머뭅니다. 선택은 그 부분공간을 벗어나는 상태, 예를 들어 노이즈로 인한 경우에만 의미가 있습니다. 그 경우 사영된 연산자는 사영이 실패하기 때문에 발전을 억제하는 반면, 사영되지 않은 연산자는 계속 작용합니다. 어느 쪽 동작도 물리적 요구 사항은 아닙니다. 사영되지 않은 연산자는 원-핫 부분공간에서만 정의되므로, 이를 선택하는 것은 자유로운 최적화가 아니라 모델링 결정입니다.

사영 연산자의 비용은 게이트입니다. 하나의 모드에서 에르미트 b†+bb^\dagger + b를 짧은 시간 동안 발전시키고, Qiskit의 트랜스파일 후 2큐비트 게이트 수를 세어 보세요.

>>> from qiskit import QuantumCircuit, transpile
>>> from qiskit.circuit.library import PauliEvolutionGate
>>>
>>> def cx_count(observable):
... circuit = QuantumCircuit(observable.num_qubits)
... circuit.append(PauliEvolutionGate(observable, time=0.1), range(observable.num_qubits))
... basis = ["cx", "rz", "sx", "x"]
... return transpile(circuit, basis_gates=basis, optimization_level=1).count_ops().get("cx", 0)
>>>
>>> hermitian = b + b_dagger
>>> levels = (3, 4, 5, 6)
>>> counts = {
... str(encoding): [
... cx_count(occupation_encoding(hermitian, LevelSpec(d - 1, encoding))) for d in levels
... ]
... for encoding in encodings
... }

정확한 개수는 Qiskit이 발전 게이트를 합성하는 방식에 따라 달라지고 이는 릴리스마다 바뀌므로, 그래프로 그려 보세요.

CX count against the number of levels d for the four encodings. The unary_projected count grows fastest, the unary_unprojected count grows linearly, and the Gray code is the cheapest.

사영되지 않은 쪽의 개수는 dd에 따라 선형으로 늘어나는데, d−1d - 1개의 전이 각각이 2큐비트 회전이기 때문입니다. 사영된 쪽의 개수는 모든 항이 dd개의 큐비트 모두에 작용하므로 훨씬 빠르게 늘어납니다. 압축 인코딩은 큐비트가 더 적게 필요하지만, 그 항의 가중치는 이웃한 준위의 비트 패턴이 어떻게 다른지에 달려 있습니다. 한 비트만 다른 Gray 코드가 두 압축 인코딩 중 더 저렴하며, 표준 이진수는 올림으로 여러 비트가 한꺼번에 뒤집힐 때(d=5d = 5) 급등합니다. 압축 인코딩이 사영되지 않은 유나리 인코딩보다도 나은지는 dd와 Qiskit이 발전을 합성하는 방식에 달려 있으므로, 한 크기에서의 순위가 아니라 추세를 읽으세요.

사영 연산자는 매핑된 연산자를 파울리로 전개할 때도 중요한데, from_sparse_observable()은 각 사영 연산자를 두 파울리 연산자의 합으로 바꾸는 방식으로 이를 수행합니다. 사영 연산자가 kk개인 항은 2k2^k개의 파울리 항이 됩니다. 다음은 작은 dd에서의 단일 b†b^\dagger이며, 이것이 이를 시도해 볼 만한 유일한 크기입니다.

>>> rows = [f"{'d':>2s} {'stored (both)':>14s} {'expanded, projected':>20s} {'expanded, unprojected':>22s}"]
>>> for levels in (3, 4, 5):
... stored = len(occupation_encoding(b_dagger, LevelSpec(levels - 1, "unary_projected")))
... expanded = [
... len(SparsePauliOp.from_sparse_observable(
... occupation_encoding(b_dagger, LevelSpec(levels - 1, encoding))).simplify())
... for encoding in ("unary_projected", "unary_unprojected")
... ]
... rows.append(f"{levels:2d} {stored:14d} {expanded[0]:20d} {expanded[1]:22d}")
>>> print("\n".join(rows))
d stored (both) expanded, projected expanded, unprojected
3 8 16 8
4 12 48 12
5 16 128 16

두 변형 모두 저장하는 항의 수는 같지만, 사영된 쪽의 전개는 dd에 대해 지수적으로 늘어납니다. 장난감 예제보다 큰 경우에는 변환하지 말고, 위의 목록처럼 SparseObservable의 항을 직접 읽으세요.

압축 인코딩은 사영 연산자를 없앨 수 없습니다. 거기서는 0 비트가 준위 번호의 일부이므로, ∣0⟩⟨0∣\lvert 0 \rangle\langle 0 \rvert를 제거하면 항이 연결하는 준위가 바뀌고, 연산자는 코드워드에서 더 이상 같지 않게 됩니다. 이 때문에 유나리 인코딩만 두 가지 변형을 가집니다.

사다리 연산자의 곱은 먼저 절단됩니다​

대각 항의 비용에서 설계상의 선택이 하나 더 드러납니다. 매퍼는 각 사다리 연산자를 매핑한 뒤 결과를 곱하지 않습니다. 한 항이 하나의 모드에 적용하는 사다리 연산자들을 모아 Fock 기저에서 d×dd \times d 행렬로 곱하고, 그 곱을 한 번 절단한 뒤 결과를 인코딩합니다 [2]. 따라서 수 연산자 b†bb^\dagger b 같은 곱은 대각으로 유지됩니다.

>>> number_observable = occupation_encoding(number, LevelSpec(3, "unary_unprojected"))
>>> [(label, qubits) for label, qubits, _ in number_observable.to_sparse_list()]
[('1', [1]), ('1', [2]), ('1', [3])]
>>> cx_count(number_observable)
0

그 항은 0보다 높은 각 준위의 큐비트에 대한 사영 연산자 ∣1⟩⟨1∣\lvert 1 \rangle\langle 1 \rvert이며, 준위 번호로 가중됩니다(준위 0은 가중치가 0이므로 항이 없습니다). 이의 시간 발전은 단일 큐비트 회전의 집합이며 2큐비트 게이트가 필요하지 않습니다. 매핑된 두 사다리 연산자를 대신 곱하면 코드워드에서는 같은 연산자를 얻지만 그 밖에서는 다른 연산자가 되며, 게이트 비용이 드는 비대각 문자열이 생깁니다.

>>> unprojected = LevelSpec(3, "unary_unprojected")
>>> as_pauli = lambda operator: SparsePauliOp.from_sparse_observable(
... occupation_encoding(operator, unprojected, 4)).simplify()
>>> composed = as_pauli(b_dagger).dot(as_pauli(b)).simplify()
>>> direct = as_pauli(number)
>>> len(direct), len(composed)
(4, 16)
>>> cx_count(direct)
0
>>> bool(cx_count(composed) > 0)
True
>>> codewords = [LevelEncoding.UNARY_UNPROJECTED.level_to_bits(level) for level in range(4)]
>>> bool(np.allclose(direct.to_matrix()[np.ix_(codewords, codewords)],
... composed.to_matrix()[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(direct.to_matrix(), composed.to_matrix()))
False

Bose-Hubbard 모델의 온사이트 상호작용 같은 대각 항이 바로 이런 곱이며, 이 선택이 중요해지는 곳입니다.

사용자 정의 매퍼 작성하기​

라이브러리가 제공하지 않는 인코딩이 필요하다면, 단일 사다리 연산자의 상(image)으로부터 만드세요. map_boson_action_generators()는 하나의 BosonAction을 매핑하는 함수와 항등원을 반환하는 함수를 받아, 이들로부터 전체 연산자의 매퍼를 만듭니다. 각 항의 작용들의 상을 순서대로 곱하고, 계수로 스케일링한 다음, 항들을 합합니다. 출력은 스칼라 곱셈과 합성을 지원하는 어떤 타입이든 될 수 있습니다.

예로, 모드를 큐비트 하나에 저장하는 d=2d = 2의 인코딩을 생각해 봅시다. 생성 연산자는 ∣1⟩⟨0∣\lvert 1 \rangle\langle 0 \rvert이며 Qiskit의 규약에서는 (X−iY)/2(X - iY)/2이고, 소멸 연산자는 그 수반 연산자입니다.

>>> from qiskit_bosons.mappers import map_boson_action_generators
>>> from qiskit_bosons.operators import BosonAction
>>>
>>> num_qubits = 2
>>>
>>> def map_action(action: BosonAction) -> SparsePauliOp:
... is_creation, mode = action
... sign = -1j if is_creation else 1j
... return SparsePauliOp.from_sparse_list(
... [("X", [mode], 0.5), ("Y", [mode], 0.5 * sign)], num_qubits=num_qubits
... )
>>>
>>> def identity() -> SparsePauliOp:
... return SparsePauliOp.from_sparse_list([("", [], 1.0)], num_qubits)
>>>
>>> model = BosonOperator.from_dict({
... (cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0, (ann(0), cre(0)): 0.7, (cre(1), ann(1)): 0.3,
... })
>>> custom = map_boson_action_generators(model, map_action, identity)
>>> library = SparsePauliOp.from_sparse_observable(occupation_encoding(model, LevelSpec(1, "gray")))
>>> bool(np.allclose(custom.to_matrix(), library.to_matrix()))
True

이 직접 작성한 매퍼는 매핑된 사다리 연산자를 곱하며, 여기서는 모든 모드가 두 개의 준위를 가지고 큐비트 하나가 모드 전체를 담기 때문에 라이브러리 매퍼와 일치합니다. 앞 절에서 보았듯이 dd가 더 크면, 매핑된 사다리 연산자를 합성하는 것과 곱을 먼저 절단하는 것은 코드워드 밖에서는 같은 연산자가 아닙니다.

이러한 매퍼를 회로에서 사용하려면 (operator, num_qubits) -> SparseObservable 시그니처를 가진 함수로 감싸서 MapperFnEvolutionSynthesis에 넘기세요. 이것은 모든 Evolution 게이트의 연산자를 그 함수로 매핑합니다. 트랜스파일러 파이프라인은 보손 회로 트랜스파일하기에 설명되어 있습니다.

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_bosons.transpiler.passes import MapperFnEvolutionSynthesis
>>>
>>> def mapper_fn(operator, num_qubits):
... return SparseObservable.from_sparse_pauli_op(
... map_boson_action_generators(operator, map_action, identity)
... )
>>>
>>> synthesis = MapperFnEvolutionSynthesis(mapper_fn)

알아 둘 세부 사항이 두 가지 있습니다. 첫째, PauliEvolutionGate는 허수부를 거부하므로 큐비트 연산자의 계수는 실수여야 합니다. 에르미트 연산자는 실수 연산자로 매핑되지만, 상쇄되는 항들이 합쳐진 뒤에만 그렇습니다. 이 직접 작성한 매퍼는 그것들을 합치지 않으므로, 호핑 항과 그 수반 항은 결과를 단순화할 때까지 허수 계수를 남깁니다. 라이브러리 매퍼는 자신의 결과를 합치지만, 사용자 정의 매퍼는 직접 해야 하며, 예를 들어 simplify()로 감싸면 됩니다.

>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> hermitian_hopping = hopping + hopping.adjoint()
>>> raw = mapper_fn(hermitian_hopping, 2)
>>> bool(np.allclose(np.asarray(raw.coeffs).imag, 0))
False
>>> merged = raw.simplify()
>>> bool(np.allclose(np.asarray(merged.coeffs).imag, 0))
True
>>> sorted(label for label, _, _ in merged.to_sparse_list())
['XX', 'YY']

둘째, 곱 공식은 항을 전달받은 순서대로 합성하는데, 매퍼가 만들어 내는 순서가 안정적이라는 보장은 없습니다. 도우미 함수 simplify()와 group_wise()는 매퍼 함수를 감싸서 이를 제어합니다. simplify는 중복된 문자열을 합치고 항을 정규 순서로 정렬하며, group_wise는 연산자의 한 그룹씩 매핑해서 그룹의 항들이 인접하게 유지되도록 합니다. 정렬이 그룹화를 버리므로 둘을 함께 사용하지 마세요. 그룹화 가이드는 그룹을 할당하는 방법을, 합성 가이드는 순서가 회로에 미치는 영향을 보여 줍니다.

다음 단계​

[1]

R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.

[2] (1,2)

N. P. D. Sawaya, T. Menke, T. H. Kyaw, S. Johri, A. Aspuru-Guzik, and G. G. Guerreschi, Resource-efficient digital quantum simulation of d-level systems for photonic, vibrational, and spin-s Hamiltonians, npj Quantum Information 6, 49 (2020), arXiv:1909.12847.