보손 회로 트랜스파일하기
이 가이드는 BosonicCircuit을 표준 QuantumCircuit으로 트랜스파일하는 방법을 설명합니다. 보손 회로에는 절단도, 인코딩도, 큐비트 수도 없으므로(보손 회로 다루기 참조), 이러한 선택은 Transpiler에서 이루어집니다. 절단과 인코딩은 LevelSpec으로 기술되고(LevelSpec으로 절단 기술하기 참조), 연산자를 큐비트 연산자로 바꾸는 매퍼는 연산자 표현 매핑하기에서 설명합니다. 결과는 일반적인 QuantumCircuit이므로 Qiskit과 호환되는 어떤 시뮬레이터로도 실행할 수 있습니다.
이 가이드는 하나의 작은 예제를 끝까지 사용합니다. 두 개의 보손 모드, 각각 보손 하나씩 있는 상태, 그리고 결합 항과 온사이트 항이 별도의 그룹에 있는 보스-허바드 해밀토니안에 의한 단일 시간 발전입니다(연산자 항 그룹화하기: 연산자 구조 활용 참조).
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> terms = {(cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0}
>>> terms.update({(cre(mode), cre(mode), ann(mode), ann(mode)): 0.5 for mode in range(2)})
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> # the bond is group 0 and the on-site interaction is group 1; derive the index from the term,
>>> # because the stored order of a dictionary is not specified
>>> hamiltonian.groups = [
... 0 if len({mode for _, mode in term}) == 2 else 1 for term, _ in hamiltonian.iter_terms()
... ]
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(Evolution(2, hamiltonian, time=0.1), circuit.modes)
>>> dict(circuit.count_ops())
{'InitializeModes': 1, 'Evolution': 1}
트랜스파일 단계
qiskit_bosons.transpiler에서 설명한 대로, 이 패키지의 프리셋 패스 매니저는 트랜스파일을 다음 단계로 나눕니다.
입력
회로를 방향 비순환 그래프(DAG) 데이터 구조로 변환하며, 이후의 모든 패스는 이 표현 위에서 작동합니다.
최적화
회로를 보손 공간에 둡니다. 이 패스들은 아직 큐비트 정보가 없는 상태에서 보손 연산자의 구조, 특히 그룹을 활용합니다. B2BSynthesis는 여기서 게이트를 분해하는 패스이며 게이트 종류마다 플러그인이 있습니다. Evolution에는 곱 공식이나 qDRIFT, ModeRotation에는 클레멘츠 메시가 쓰입니다(방법이 선택되는 과정은 보손 공간에서 Evolution 게이트 합성하기 참조). RelabelModes는 모드의 순서를 바꿉니다. 프리셋은 이 단계에서 모드 회전을 분해하고 나머지는 그대로 둡니다.
레이아웃
각 보손 모드 레지스터를 양자 레지스터에 매핑합니다. 보손 모드는 일반적으로 여러 큐비트를 차지하므로(유니태리 인코딩에서는 개, 압축 인코딩에서는 개), 양자 레지스터가 모드 레지스터보다 넓습니다. OccupationB2QLayout은 LevelSpec으로부터 레지스터 폭을 도출합니다.
합성
보손 게이트를 큐비트 연산으로 변환하며, 게이트 종류마다 플러그인이 하나씩 있습니다. 인코딩은 이 단계에서 이루어집니다. Evolution 게이트의 플러그인은 그 연산자를 큐비트 연산자로 매핑하고, InitializeModes 게이트의 플러그인은 각 점유를 인코딩하는 큐비트를 설정합니다. 배리어는 그것이 걸쳐 있던 각 레지스터의 모든 큐비트로 옮겨집니다.
큐비트
결과로 얻은 큐비트 회로를 예를 들어 기저 게이트 집합이나 디바이스에 맞게 Qiskit 자체의 방법으로 트랜스파일합니다. 프리셋에는 항상 이 단계가 포함되며, 기본값은 모든 시간 발전을 게이트로 합성하는 Qiskit의 프리셋 패스 매니저입니다(큐비트 수준에서 이어 가기 참조).
출력
내부 DAG 데이터 구조를 다시 QuantumCircuit으로 변환합니다.
절단과 인코딩은 회로에 저장되지 않습니다. 이들은 Transpiler에 주어지며, 레이아웃, 상태 준비, 매퍼가 모두 이에 합의해야 합니다. 프리셋은 같은 LevelSpec을 세 곳 모두에 전달하여 이를 보장합니다.
실용적인 예
generate_preset_occupation_pass_manager()는 스펙으로부터 파이프라인을 만듭니다. 예제를 두 가지 인코딩으로 낮추려면 스펙만 바꾸면 됩니다. 프리셋은 시간 발전을 게이트로 합성하는 Qiskit 자체의 큐비트 수준 트랜스파일로 끝납니다. 보손 단계가 내보낸 그대로의 회로를 보려면 먼저 그 단계를 비우세요.
>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> for encoding in ("unary_unprojected", "gray"):
... pass_manager = generate_preset_occupation_pass_manager(LevelSpec(2, encoding))
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(circuit)
... print(f"{encoding:18s} qubits={lowered.num_qubits} ops={dict(lowered.count_ops())}")
unary_unprojected qubits=6 ops={'x': 2, 'PauliEvolution': 1}
gray qubits=4 ops={'x': 2, 'PauliEvolution': 1}
두 인코딩은 각각 여섯 개와 네 개의 큐비트를 사용하지만(유니태리 인코딩에서는 모드당 세 레벨, 그레이 코드에서는 모드당 큐비트 두 개), 회로의 구조는 같습니다. 두 개의 XGate 연산이 점유를 준비하고, 하나의 PauliEvolutionGate가 시간 발전을 담당합니다.
기존 워크플로와 비교
보손 회로 가이드는 QuantumCircuit을 만들기 전에 해밀토니안을 큐비트 연산자로 매핑하는 기존 워크플로를 설명합니다. 두 경로는 동등한 회로를 만듭니다. 차이는 절단이 바뀔 때 다시 해야 하는 일에 있습니다. 보손 회로는 한 번 만들어 모든 컷오프에서 낮추면 되지만, 매핑을 먼저 하는 경로는 연산자를 다시 매핑하고 회로를 다시 만들어야 합니다.
>>> from qiskit import QuantumCircuit
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>>
>>> basis = ["rz", "sx", "x", "cx"]
>>> cx_counts = []
>>> for max_occupation in (1, 2, 3):
... spec = LevelSpec(max_occupation, "gray")
... # the bosonic circuit is reused as it is
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... # the map-first route maps the operator and builds a new circuit for every cutoff
... qubit_hamiltonian = occupation_encoding(hamiltonian, spec)
... by_hand = QuantumCircuit(qubit_hamiltonian.num_qubits)
... _ = by_hand.append(PauliEvolutionGate(qubit_hamiltonian, time=0.1), by_hand.qubits)
... cx_counts.append(lowered.count_ops().get("cx", 0))
... print(
... f"d={max_occupation + 1}: {lowered.num_qubits} qubits; map-first: "
... f"{len(qubit_hamiltonian)} Pauli terms to place by hand"
... )
d=2: 2 qubits; map-first: 2 Pauli terms to place by hand
d=3: 4 qubits; map-first: 10 Pauli terms to place by hand
d=4: 4 qubits; map-first: 22 Pauli terms to place by hand
얽힘 게이트 수는 Qiskit이 시간 발전 게이트를 합성하는 방식에 따라 달라지며 이는 릴리스마다 바뀌므로, 플롯으로 확인하세요.
게이트 수는 컷오프에 따라 빠르게 증가하며, 그래서 컷오프를 회로가 아니라 트랜스파일의 매개변수로 두는 것입니다. 두 경로는 여기서는 동등하지만, 보손 경로만이 연산자의 그룹화를 곱 공식과 엮거나(보손 공간에서 Evolution 게이트 합성하기 참조), 회로를 한 줄도 바꾸지 않고 다른 스펙에 인코딩을 넘길 수 있습니다.
컷오프가 하나인 스펙은 모드 수를 열어 두므로, 하나의 패스 매니저가 어떤 폭의 회로든 처리합니다. 모드마다 컷오프가 있는 스펙은 폭을 고정합니다.
>>> wide = BosonicCircuit(3)
>>> wide.append(InitializeModes([1, 0, 1]), wide.modes)
>>> shared = generate_preset_occupation_pass_manager(LevelSpec(2, "gray"))
>>> shared.run(wide).num_qubits
6
>>> per_mode = generate_preset_occupation_pass_manager(LevelSpec([2, 1], "gray"))
>>> per_mode.run(circuit).num_qubits
3
>>> try:
... per_mode.run(wide)
... except ValueError as error:
... print(error)
the spec covers 2 modes, but 3 were expected
파이프라인을 직접 구성하기
프리셋은 단계마다 항목이 하나씩 있는 MultiStagePassManager입니다. 같은 파이프라인을 각 부분으로 직접 조립할 수도 있으며, 이는 아래의 모든 맞춤 설정의 출발점입니다.
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import BosonicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_bosons.transpiler.passes import (
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> spec = LevelSpec(2, "gray")
>>>
>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>>
>>> by_hand = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> preset = generate_preset_occupation_pass_manager(spec)
>>> preset.qubit = PassManager()
>>> by_hand.run(circuit) == preset.run(circuit)
True
매퍼 함수는 연산자와 레이아웃이 도출한 레지스터의 너비를 받아 SparseObservable을 반환합니다. 너비가 주어지기 때문에 num_qubits를 occupation_encoding()에 전달하면 매퍼가 스펙이 레지스터에 맞는지 확인하며, 레이아웃과 인코딩이 일치하지 않으면 명확한 메시지와 함께 실패합니다.
고급 워크플로
명시적 레이아웃 제공
CustomB2QLayout은 레이아웃 단계를 회로의 각 보손 레지스터에서 양자 레지스터로 가는 사용자 지정 매핑으로 대체합니다. 레지스터가 스펙에서 필요로 하는 것보다 넓어야 할 때, 예를 들어 다른 용도로 유휴 큐비트를 남겨 두려는 경우에 유용합니다.
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit_bosons.transpiler.passes import CustomB2QLayout
>>>
>>> (mode_register,) = BosonicCircuitToDAG().run(circuit).qregs.values()
>>> custom = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=CustomB2QLayout({mode_register: QuantumRegister(6, "q")}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> custom.run(circuit).num_qubits
6
사용자 지정 레이아웃은 사용자의 책임입니다. 합성 플러그인은 여전히 스펙을 통해 큐비트에 접근하므로, 스펙의 너비와 맞지 않는 레지스터는 잘못된 회로를 만들거나 플러그인 내부 깊은 곳에서 오류를 일으킵니다. 또한 사용자 지정 레이아웃은 RelabelModes 이후에 OccupationB2QLayout이 수행하는 검사도 건너뜁니다(다음 섹션 참조).
모드 순서 바꾸기
RelabelModes는 모드의 순서를 바꾸는 최적화 단계 패스입니다. 순열은 원래 모드 인덱스에서 새 모드 인덱스로 가는 매핑으로 해석되며 회로 메타데이터에 기록됩니다. 순서를 바꾸면 각 모드가 차지하는 큐비트가 달라지므로 낮춘(lowered) 회로의 깊이를 줄일 수 있습니다.
결과가 새 모드 순서로 표현되므로, 측정된 비트열은 해석하기 전에 다시 원래대로 매핑해야 합니다. 모드당 큐비트가 여러 개이면 매핑은 개별 비트가 아니라 큐비트 블록 단위로 작용합니다. 다음 예제는 Gray 코드에서 각각 큐비트 두 개를 차지하는 모드 세 개의 순서를 바꾸고, 측정된 카운트에서 원래의 점유 상태를 복원합니다.
>>> # BasicSimulator is a convenient pure-Python choice; any simulator works
>>> from qiskit.providers.basic_provider import BasicSimulator
>>> from qiskit_bosons.transpiler.passes import RelabelModes
>>>
>>> state = BosonicCircuit(3)
>>> state.append(InitializeModes([2, 0, 1]), state.modes)
>>> spec = LevelSpec(2, "gray") # three levels, two qubits per mode
>>> width = spec.resolve(3).widths[0]
>>> permutation = [2, 0, 1]
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> relabeled = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> relabeled.metadata
{'permutation': [2, 0, 1]}
>>>
>>> def restore(bitstring, permutation, width):
... """Map a bitstring in the new mode order back to the original mode order."""
... qubits = bitstring[::-1] # character q is now the value of qubit q
... blocks = [qubits[mode * width : (mode + 1) * width] for mode in range(len(permutation))]
... return "".join(blocks[new] for new in permutation)[::-1]
>>>
>>> relabeled.measure_all()
>>> (measured,) = BasicSimulator().run(relabeled, shots=1).result().get_counts()
>>> measured
'110100'
>>> restore(measured, permutation, width)
'010011'
복원된 비트열은 순서 바꾸기 없이 회로가 만들어 내는 결과를 Qiskit의 리틀 엔디언 순서로 읽은 것입니다. 모드 0은 레벨 2의 Gray 코드(11), 모드 1은 레벨 0(00), 모드 2는 레벨 1(01)을 가집니다.
>>> plain = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> plain.measure_all()
>>> (reference,) = BasicSimulator().run(plain, shots=1).result().get_counts()
>>> restore(measured, permutation, width) == reference
True
모드마다 서로 다른 컷오프를 지정하는 스펙은 RelabelModes와 함께 사용할 수 없습니다. 컷오프는 위치로 모드에 연결되므로 모드 순서를 바꾸면 컷오프가 엉뚱한 모드에 적용됩니다. 이 경우 OccupationB2QLayout은 순서가 바뀐 회로를 거부합니다.
>>> mixed = LevelSpec([2, 1, 2], "gray")
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(mixed)
>>> refused = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(mixed),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> try:
... refused.run(state)
... except ValueError as error:
... print(str(error).split(",")[0])
the circuit has been relabeled by RelabelModes
합성 플러그인 선택
B2QSynthesis에는 기본 플러그인이 없으며, 프리셋이 게이트 유형마다 하나씩 등록합니다. 사용 가능한 플러그인은 B2QSynthesisPluginManager가 관리하며, 이 관리자는 플러그인을 대상 게이트의 이름별로 나열합니다.
>>> from qiskit_bosons.transpiler.passes import B2QSynthesisPluginManager
>>> manager = B2QSynthesisPluginManager()
>>> for gate in manager.op_names():
... print(gate, manager.method_names(gate))
Evolution ['MapperFn']
InitializeModes ['Occupation']
기본 제공 메서드는 Evolution.MapperFn과 InitializeModes.Occupation 두 가지입니다. 첫 번째는 임의의 매퍼 함수로 발전(evolution)을 합성하며, 사용자 지정 인코딩을 연결하는 방법이 바로 이것입니다(연산자 표현 매핑 참조). 라이브러리에는 ModeRotation 게이트용 플러그인이 포함되어 있지 않습니다. 프리셋은 먼저 보손 공간에서 이를 Evolution 게이트로 분해하고 첫 번째 메서드가 이를 처리하지만, ModeRotation용으로 등록한 플러그인은 대신 이 게이트를 직접 낮출 수 있습니다. 보손 모드 회전과 분해 없이 회전 낮추기를 참조하세요. B2QSynthesis의 config 인수를 통해 이름으로 메서드를 등록하거나, 앞선 예제처럼 B2QSynthesis.methods에 플러그인 객체를 직접 작성할 수 있습니다.
큐비트 수준에서 이어가기
프리셋은 항상 Qiskit 자체의 프리셋 패스 매니저인 qubit 단계로 끝납니다. generate_preset_occupation_pass_manager()의 키워드 인수는 qiskit.transpiler.generate_preset_pass_manager()로 전달되므로, 기저 게이트 집합으로 변환하거나 장치를 직접 대상으로 지정할 수 있습니다. 보손 단계에서 만들어진 회로를 그대로 유지하려면 optimization_level=0을 사용하는 것이 좋습니다.
>>> from qiskit.transpiler import generate_preset_pass_manager
>>>
>>> spec = LevelSpec(2, "gray")
>>> translated = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ).run(circuit)
>>> sorted(translated.count_ops())
['cx', 'rz', 'sx', 'x']
흔한 선택이 두 가지 더 있습니다. 위의 예제처럼 파이프라인이 합성한 그대로의 회로를 보여 주고 싶다면 qubit 단계에 빈 PassManager를 지정하세요. 또는 파이프라인을 직접 조립해 임의의 Qiskit 패스 매니저를 MultiStagePassManager의 qubit 단계로 추가할 수도 있습니다. 단계는 주어진 순서대로 실행되므로 qubit 단계는 output보다 앞에 와야 합니다. Qiskit의 패스는 완성된 회로가 아니라 DAG를 기대하기 때문입니다.
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits)
... )
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> staged = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ),
... output=QuantumDAGToCircuit(),
... )
>>> sorted(staged.run(circuit).count_ops())
['cx', 'rz', 'sx', 'x']
다음 단계
-
연산자 표현 매핑: 인코딩과 사용자 지정 매퍼 작성 방법을 설명합니다.
-
보손 공간에서 Evolution 게이트 합성: 발전 게이트에 대한 곱 공식을 선택하는 방법을 설명합니다.
-
보손 모드 회전: 모드 회전을 낮추는 방법을 설명합니다.
-
인코딩을 선택하여 Bose-Hubbard 동역학 시뮬레이션: 낮춘 회로를 정확한 동역학과 비교해 검증하는 완전한 시뮬레이션을 다룹니다.
-
qiskit_bosons.transpiler: 단계와 사용 가능한 패스에 대한 전체 설명입니다.