보손 공간에서 Evolution 게이트 합성
서로 교환되지 않는 조각들의 합인 해밀토니안의 시간 발전 는 게이트 단위로 정확하게 구현할 수 없습니다. 곱 공식은 이를 개별 조각들 아래의 짧은 발전들의 열로 근사하며, 이 근사 오차를 트로터 오차라고 부릅니다.
곱 공식은 두 곳에 적용할 수 있으며, 둘은 서로 바꿔 쓸 수 없습니다.
-
보손 공간에서, 절단이나 인코딩이 선택되기 전에 적용합니다. 공식은 연산자의 그룹을 보고(연산자 항 묶기: 연산자 구조 활용 참조) 이들을 교차 배치할 수 있습니다.
-
큐비트 공간에서, 연산자가 매핑된 후 Qiskit의
PauliEvolutionGate내부에서 적용합니다. 공식은 그룹마다 파울리 연산자 하나를 보며, 그 안의 항들만 세분할 수 있습니다.
교환되지 않는 그룹 사이의 오차, 보통 가장 지배적인 오차를 줄일 수 있는 것은 첫 번째뿐입니다. 이 가이드는 그 사용법을 보여줍니다.
두 단계 모두 선택 사항입니다. 보손 공간에서 분해되지 않은 Evolution 게이트는 통째로 보손-큐비트 단계에 도달하여 거기서 매핑되고 합성됩니다. 이 경우 보손 수준에서는 오차가 전혀 발생하지 않으며, 큐비트 수준 공식이 도입하는 오차만 있습니다.
설정
2사이트 보스-허バード 체인은 그룹들이 교환되지 않는 가장 작은 예제입니다(전체 모델은 인코딩을 선택하여 보스-허バード 동역학 시뮬레이션 참조). 결합에 대한 에르미트 그룹 하나와 온사이트 상호작용에 대한 대각 그룹 하나로 묶여 있습니다.
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp, Statevector
>>> from qiskit.transpiler import PassManager
>>> from scipy.sparse import csc_matrix
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> num_sites, max_occupation, total_time = 2, 2, 0.8
>>> bonds = [(0, 1)]
>>> terms = {}
>>> for left, right in bonds:
... terms[(cre(left), ann(right))] = -1.0
... terms[(cre(right), ann(left))] = -1.0
>>> for site in range(num_sites):
... terms[(cre(site), cre(site), ann(site), ann(site))] = 1.0
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> hamiltonian.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in hamiltonian.iter_terms()
... ]
>>> hamiltonian.num_groups()
2
오차를 측정하려면 기준이 필요합니다. 절단은 별개의 근사이므로, 같은 절단에서 매핑된 해밀토니안의 정확한 발전이 올바른 기준입니다.
>>> spec = LevelSpec(max_occupation, "unary_projected")
>>> mapped = occupation_encoding(hamiltonian, spec).simplify()
>>> matrix = csc_matrix(SparsePauliOp.from_sparse_observable(mapped).to_matrix(sparse=True))
>>> pm = generate_preset_occupation_pass_manager(spec)
>>> pm.qubit = PassManager() # keep each group as one exact PauliEvolutionGate
>>>
>>> def build(**evolution_kwargs):
... circuit = BosonicCircuit(num_sites)
... circuit.append(InitializeModes([1, 1]), circuit.modes)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, **evolution_kwargs), circuit.modes
... )
... return circuit
>>>
>>> # Under the unary encodings, occupation ``n`` of mode ``m`` sets qubit ``m * d + n``, with
>>> # ``d = max_occupation + 1`` levels per mode. That places |1, 1> on the basis state below.
>>> levels = max_occupation + 1
>>> index = sum(1 << (mode * levels + n) for mode, n in enumerate([1, 1]))
>>> initial = Statevector.from_int(index, 2**(num_sites * levels)).data
>>> exact = expm_multiply(-1j * total_time * matrix, initial)
>>> def error(circuit):
... """Distance to the exact evolution, of the circuit's product of group exponentials."""
... lowered = pm.run(circuit)
... state = Statevector.from_int(0, 2**lowered.num_qubits).evolve(lowered).data
... return float(np.max(np.abs(state - exact)))
error는 낮춰진 회로를 PauliEvolutionGate 인스턴스를 분해하지 않고 의도적으로 시뮬레이션합니다. Statevector는 각각을 해당 그룹의 정확한 지수로 적용하므로, 남는 것은 보손 합성이 선택한 그룹 지수들의 곱뿐입니다.
기본값
synthesis 인수가 없으면 Evolution 게이트는 1차 공식인 BosonicLieTrotter로 분해됩니다. 그룹마다 하나의 인자가 있고, 각각 전체 시간 동안 적용됩니다.
>>> dict(build().decompose().count_ops())
{'Evolution': 2, 'InitializeModes': 1}
분해하지 않으면 게이트 전체가 큐비트 단계로 전달되며 보손 트로터 오차는 전혀 없습니다.
>>> error(build()) < 1e-6
True
분해하면 두 그룹이 교환되지 않기 때문에 실제 오차가 나타납니다.
>>> error(build().decompose()) > 0.1
True
오차 줄이기
게이트에 다른 합성 방법을 전달하세요. 더 높은 차수는 인자들을 대칭적으로 합성하여 주요 오차 항이 상쇄되게 하고, 더 많은 반복은 시간을 더 짧은 단계로 나눕니다.
>>> from qiskit_bosons.circuit.library.synthesis import BosonicLieTrotter, BosonicSuzukiTrotter
>>>
>>> for label, synthesis in [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ("Suzuki, order 4*", BosonicSuzukiTrotter(order=4, merge_adjacent=False)),
... ]:
... circuit = build(synthesis=synthesis).decompose()
... print(f"{label:16s} {circuit.count_ops()['Evolution']:3d} factors")
Lie-Trotter 2 factors
Suzuki, order 2 3 factors
Suzuki, order 4 11 factors
Suzuki, order 4* 15 factors
차수가 2 올라갈 때마다 아래 차수의 스윕 다섯 개가 합성됩니다(차수 2의 개 인자에 적용된 스즈키 재귀의 증가). 이것이 별표가 표시된 마지막 줄의 개수입니다. 스윕들이 같은 인자에서 만나므로 이웃한 게이트들은 하나의 연산자 아래에서 발전하며, 시간을 합산한 하나의 게이트로 융합됩니다. 이렇게 하면 이음새마다 게이트 하나가 절약되며 정확합니다. 교과서적인 시퀀스를 유지하려면 merge_adjacent=False를 전달하세요. 어느 쪽이든 더 높은 차수는 더 작은 오차를 얻게 해줍니다.
>>> lie = error(build(synthesis=BosonicLieTrotter()).decompose())
>>> second = error(build(synthesis=BosonicSuzukiTrotter(order=2)).decompose())
>>> fourth = error(build(synthesis=BosonicSuzukiTrotter(order=4)).decompose())
>>> lie > second > fourth
True
반복은 다른 비율로 깊이와 정확도를 맞바꿉니다. 1차 오차는 단계 크기에 대해 선형으로, 2차 오차는 이차로 감소합니다.
>>> lie_errors = [error(build(synthesis=BosonicLieTrotter(reps=r)).decompose()) for r in (1, 2, 4, 8)]
>>> bool(all(a > b for a, b in zip(lie_errors, lie_errors[1:])))
True
>>> bool(lie_errors[0] / lie_errors[-1] > 6) # about 8, i.e. linear in 1 / reps
True
>>> suzuki_errors = [
... error(build(synthesis=BosonicSuzukiTrotter(order=2, reps=r)).decompose()) for r in (1, 2, 4)
... ]
>>> bool(suzuki_errors[0] / suzuki_errors[-1] > 10) # about 16, i.e. quadratic in 1 / reps
True
어느 쪽이 더 나은 교환인지는 연산자에 따라 다릅니다. 그룹이 하나뿐인 연산자는 어떤 차수에서도 정확하다는 점에 유의하세요. 단독 인자는 자기 자신과 교환되며, 공식은 이를 다시 분할하지 않고 게이트 하나로 축약됩니다.
비용 측정하기
오차가 작을수록 인자가 더 많이 필요하고, 각 인자는 큐비트 단계에서 합성되면 얽힘 게이트를 소모합니다. 그 수는 인코딩에 따라 다르므로, 기저를 고정하고 큐비트 수준 최적화 없이(optimization_level=0) 프리셋 파이프라인을 통해 세어 각 방법의 비용을 확인하세요.
>>> basis = ["rz", "sx", "x", "cx"]
>>>
>>> def cx_count(circuit, encoding):
... spec = LevelSpec(max_occupation, encoding)
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... return lowered.count_ops().get("cx", 0)
>>>
>>> methods = [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ]
>>> encodings = ("gray", "unary_unprojected")
>>> counts = {
... encoding: [
... cx_count(build(synthesis=method).decompose(), encoding) for _, method in methods
... ]
... for encoding in encodings
... }
>>> # the order buys accuracy at a price that grows with it
>>> bool(counts["gray"][0] < counts["gray"][1] < counts["gray"][2])
True
정확한 개수는 Qiskit이 발전 게이트를 합성하는 방식에 따라 달라지며 릴리스마다 바뀌므로, 표 대신 플롯으로 보여줍니다.
각 막대 그룹은 하나의 방법입니다. 리-트로터, 2차 스즈키, 4차 스즈키입니다. 차수는 재귀 계수만큼 늘어나는 비용으로 정확도를 얻게 해주며, 유니터리 인코딩은 인자마다 더 많은 얽힘 게이트로 매핑되기 때문에 모든 방법에서 그레이 코드보다 더 많은 비용을 치릅니다. 필요한 정확도에 맞춰 방법을 선택하고 하드웨어에 맞춰 인코딩을 선택하되, 두 선택을 분리해 두세요. 위의 보손 회로는 바뀌지 않습니다.
하나의 방법을 전체 회로에 적용하기
각 게이트에 synthesis를 설정하려면 게이트를 만드는 모든 곳에 그 선택을 전달해야 합니다. B2BSynthesis 패스는 대신 파이프라인에 대해 선택을 한 번에 수행합니다. 그 config는 게이트 유형별로 방법을 지정하며, 방법을 선택하고 게이트를 펼치는 일을 모두 수행합니다.
>>> from qiskit_bosons.transpiler import BosonicPassManager
>>> from qiskit_bosons.transpiler.passes import B2BSynthesis
>>>
>>> trotterize = BosonicPassManager([B2BSynthesis({"Evolution": BosonicSuzukiTrotter(order=2)})])
>>> dict(trotterize.run(build()).count_ops())
{'Evolution': 3, 'InitializeModes': 1}
방법을 직접 만드는 대신 __init__의 인수와 함께 이름으로 지정할 수도 있습니다. B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}). 사용 가능한 이름은 B2BSynthesisPluginManager의 이름들이며, 어디에도 등록되지 않은 방법은 위와 같이 인스턴스로 전달할 수 있습니다.
apply=False를 사용하면 패스는 방법만 선택하고 펼치는 작업은 필요한 시점으로 미룹니다. 예를 들어 먼저 회로를 살펴볼 때 유용합니다. 원자 게이트는 건드리지 않으며 태그도 붙이지 않습니다.
>>> selected = BosonicPassManager(
... [B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)]
... ).run(build())
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
3
펼쳐지지 않은 게이트는 통째로 보손-큐비트 단계에 도달하며, 거기서 synthesis를 참조하지 않고 매핑되므로 어떤 방법이든 동일한 출력을 생성합니다.
선택적 filter는 이점이 있는 게이트로 범위를 제한합니다. 수 연산자만으로 이루어진 해밀토니안처럼 그룹이 모두 교환되는 Evolution은 어떤 차수에서도 정확하게 합성되므로, 더 높은 차수는 깊이만 늘릴 뿐입니다.
이점이 있는 게이트로 패스 제한하기
순수한 대각 연산자는 수 연산자끼리 교환되므로 이 점을 구체적으로 보여줍니다. 그 그룹들은 1차에서 정확하며, 2차는 쓸데없이 얽힘 게이트를 소모합니다.
>>> # one group per term, each Hermitian; the order is fixed because order two evolves the first
>>> # group twice, so which group comes first changes the cost
>>> diagonal = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(0), cre(1), ann(1)], 1.0, 0), ([cre(0), ann(0)], 0.5, 1)]
... )
>>>
>>> def diagonal_cost(synthesis):
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes)
... return cx_count(circuit.decompose(), "gray")
>>>
>>> bool(diagonal_cost(BosonicSuzukiTrotter(order=2)) > diagonal_cost(BosonicLieTrotter()))
True
두 회로 모두 정확하므로, 두 번째는 이득 없이 더 많은 얽힘 게이트를 소모합니다. (대칭 2차 공식은 첫 번째 그룹을 절반의 시간으로 두 번 적용하므로, 비용은 어느 그룹이 먼저 오는지에 따라 달라집니다.) filter는 DAGOpNode에 대한 조건자를 받고 거부된 게이트는 그대로 둡니다. 따라서 하나의 패스로 이점이 있는 게이트의 차수는 올리고 나머지는 1차로 유지할 수 있습니다. 아래 조건자는 연산자가 모드 사이에서 보손을 이동시킬 때, 즉 그룹들이 교환되지 않을 때에만 게이트를 허용합니다.
>>> def is_diagonal(operator):
... """Whether every term only counts bosons, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> order_2 = BosonicSuzukiTrotter(order=2)
>>> everywhere = cx_count(
... BosonicPassManager([B2BSynthesis({"Evolution": order_2})]).run(mixed_circuit()), "gray"
... )
>>> restricted = cx_count(
... BosonicPassManager(
... [
... B2BSynthesis(
... {"Evolution": order_2},
... filter=lambda node: not is_diagonal(node.op.operator),
... )
... ]
... ).run(mixed_circuit()),
... "gray",
... )
>>> bool(everywhere > restricted) # the filter saves entangling gates
True
패스를 다시 실행해도 아무것도 바뀌지 않습니다. 패스가 내놓는 인자들은 원자적이기 때문입니다.
원자적 인자
atomic으로 표시된 Evolution에는 정의가 없으므로, decompose()를 몇 번 적용해도 제자리에 그대로 남습니다. 합성은 자신이 내놓는 인자들을 이렇게 표시하며, 샘플링하는 항에 대해서는 BosonicQDrift도 마찬가지입니다.
이것은 단순한 정리 이상입니다. 에르미트 그룹을 항 단위로 더 쪼개면 개별적으로 에르미트가 아닌 인자들이 생기고, 비에르미트 연산자의 지수는 유니터리가 아닙니다.
>>> circuit = build()
>>> once = circuit.decompose()
>>> dict(circuit.decompose(reps=5).count_ops()) == dict(once.count_ops())
True
각 인자는 그 자체로 에르미트여야 합니다. 그룹으로 나뉜 에르미트 연산자가 호핑 항과 그 수반이 분리되는 것처럼 비에르미트 그룹을 만들 수 있습니다. groups_are_hermitian()로 확인하세요. 그룹이 없는 연산자는 항 단위로 분할되며, 그러면 큐비트 단계가 Operator contains complex coefficients와 함께 인자들을 거부합니다. 짝수 차수의 대칭화는 비에르미트 인자의 오차를 부분적으로 상쇄하므로, 잘못 묶인 연산자도 1차보다 2차에서 더 좋아 보일 수 있지만 여전히 틀린 것입니다.
짧은 단계 반복하기
회로 수준의 대안으로, 짧은 트로터 단계 하나를 만들어 반복하세요. repeat()는 반복들을 하나의 회로로 평탄화하고 경계를 인접하게 두므로, 패스가 그 경계를 가로질러 작동할 수 있습니다.
>>> step = BosonicCircuit(num_sites)
>>> step.append(Evolution(num_sites, hamiltonian, time=total_time / 4), step.modes)
>>> dict(step.repeat(4).count_ops())
{'Evolution': 4}
반복을 분리해 두려면 insert_barriers=True를 전달하세요. 인코딩은 모드당 여러 큐비트가 필요하고 모드에는 큐비트별 상이 없으므로, 배리어는 그것이 닿는 레지스터의 모든 큐비트에 매핑됩니다.
>>> [inst.operation.name for inst in step.repeat(3, insert_barriers=True)._inner.data]
['Evolution', 'barrier', 'Evolution', 'barrier', 'Evolution']
repeat는 합성 방법의 reps와 무관합니다. 합성은 하나의 발전을 더 많고 더 짧은 인자로 세분합니다. 하나의 발전을 담은 회로를 반복하면 총 시간이 곱해질 뿐입니다.
큐비트 수준 공식과 결합하기
두 수준은 합성할 수 있으며, 둘 다 근사인 경우 결과의 정확도는 둘 중 더 약한 쪽이 좌우합니다. 큐비트 쪽 공식이 1차로 유지되는 상태에서 보손 차수를 올려도 얻는 것이 적습니다. 큐비트 쪽 공식은 MapperFnEvolutionSynthesis를 참조하세요. 이것은 매퍼가 선택한 파울리 항 순서를 유지하므로, group_wise()는 각 그룹의 항들을 서로 인접하게 내보낼 수 있고 simplify()는 대신 표준 순서를 고정할 수 있습니다.
다음 단계
-
보손 회로 트랜스파일은 보손 합성 이후의 단계들과
B2BSynthesis가 그중 어디에 위치하는지를 설명합니다. -
연산자 항 묶기: 연산자 구조 활용은 곱 공식이 교차 배치하는 그룹을 지정하고 확인하는 방법을 설명합니다.
-
인코딩을 선택하여 보스-허バード 동역학 시뮬레이션은 반복 방식을 완전한 예제에 적용합니다.