ボソン回路のトランスパイル
このガイドでは、BosonicCircuit を標準の QuantumCircuit にトランスパイルする方法を説明します。ボソン回路は、切り捨て、エンコーディング、量子ビット数のいずれも持たないため(ボソン回路を扱うを参照)、これらの選択が行われるのはトランスパイラーです。切り捨てとエンコーディングは LevelSpec で記述され(LevelSpec で切り捨てを記述するを参照)、演算子を量子ビット演算子に変換するマッパーについては演算子表現をマッピングするで説明しています。結果は通常の QuantumCircuit なので、Qiskit 互換のシミュレーターであればどれでも実行できます。
このガイドでは一貫して1つの小さな例を使います。2つのボソンモード、各モードに1つずつボソンがある状態、そしてボンド項とオンサイト項が別々のグループにあるボース・ハバード・ハミルトニアンによる単一の時間発展です(演算子の項をグループ化する:演算子の構造を使うを参照)。
>>> 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 はモードを並べ替えます。プリセットはこの段階でモード回転を分解し、それ以外はそのままにします。
レイアウト
各ボソンモードレジスタを量子レジスタに対応付けます。1つのボソンモードは一般に複数の量子ビットを占めるため(ユニタリー・エンコーディングでは 個、コンパクト・エンコーディングでは 個)、量子レジスタはモードレジスタより幅が広くなります。OccupationB2QLayout は、LevelSpec からレジスタの幅を導きます。
合成
ボソンゲートを量子ビット演算に変換します。ゲートの種類ごとに1つのプラグインがあります。エンコーディングはこのステップで行われます。Evolution ゲートのプラグインはその演算子を量子ビット演算子に対応付け、InitializeModes ゲートのプラグインは各占有数をエンコードする量子ビットを設定します。バリアは、それが触れた各レジスタのすべての量子ビットに引き継がれます。
量子ビット
結果として得られた量子ビット回路を、Qiskit 自身の手法でトランスパイルします。たとえば、基底ゲートセットやデバイスへのトランスパイルです。プリセットには常にこれが含まれます。デフォルトでは Qiskit のプリセットパスマネージャーで、すべての時間発展をゲートに合成します(量子ビットレベルで続けるを参照)。
出力
内部の DAG データ構造を QuantumCircuit に戻します。
切り捨てとエンコーディングは回路には保存されません。これらはトランスパイラーに渡され、レイアウト、状態準備、マッパーはすべてそれらについて一致していなければなりません。プリセットは、同じ LevelSpec を3つすべてに渡すことでそれを保証します。
実用的な例
generate_preset_occupation_pass_manager() は、スペックからパイプラインを構築します。2つのエンコーディングで例を低レベル化する場合、変わるのはスペックだけです。プリセットは 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}
2つのエンコーディングはそれぞれ6量子ビットと4量子ビットを使いますが(ユニタリー・エンコーディングではモードあたり3レベル、グレイコードではモードあたり2量子ビット)、回路の構造は同じです。2つの XGate 演算が占有数を準備し、1つの PauliEvolutionGate が時間発展を担います。
従来のワークフローとの比較
ボソン回路のガイドでは、QuantumCircuit を構築する前にハミルトニアンを量子ビット演算子に対応付ける、従来のワークフローを説明しています。2つの経路は等価な回路を生成します。違いは、切り捨てが変わったときにやり直す必要がある内容です。ボソン回路は一度構築すれば、あらゆるカットオフで低レベル化できますが、マッピングを先に行う経路では、演算子を再度マッピングし、回路を再構築する必要があります。
>>> 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 が時間発展ゲートをどのように合成するかに依存し、それはリリースごとに変わるため、プロットしてみましょう。
カットオフとともに数は急速に増加します。そのため、カットオフは回路ではなくトランスパイルのパラメーターにしておく理由があります。ここでは2つの経路は等価ですが、演算子のグループ化を積公式と組み合わせたり(ボソン空間で Evolution ゲートを合成するを参照)、回路を1行も変えずにエンコーディングを別のスペックに渡したりできるのは、ボソンの経路だけです。
単一のカットオフを持つスペックでは、モードの数が未定のままなので、1つのパスマネージャーであらゆる幅の回路に対応できます。モードごとにカットオフを持つスペックでは、幅が固定されます。
>>> 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
パイプラインを手作業で構築する
このプリセットは、ステージごとに1つのエントリを持つ 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 は、モードを並べ替える最適化ステージのパスです。置換は、元のモードのインデックスから新しいモードのインデックスへのマッピングとして解釈され、回路のメタデータに記録されます。並べ替えにより、各モードが占有する量子ビットが変わるため、ロワリング後の回路の深さを減らせる場合があります。
結果は新しいモード順で表されるため、測定されたビット列は、解釈する前に元の順序へ戻す必要があります。モードごとに複数の量子ビットがある場合、マッピングは単一のビットではなく量子ビットのブロックに作用します。次の例では、グレイコードのもとでそれぞれ2つの量子ビットを占有する3つのモードを再ラベル付けし、測定されたカウントから元の占有数を復元します。
>>> # 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のグレイコード(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 にはデフォルトのプラグインがありません。プリセットがゲートの種類ごとに1つずつ登録します。利用可能なプラグインは 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 の2つです。1つ目は任意のマッパー関数から時間発展を合成するもので、カスタムエンコーディングを組み込む方法でもあります(演算子表現をマッピングするを参照してください)。このライブラリには、ModeRotation ゲート用のプラグインは含まれていません。プリセットは、まずこのゲートをボソン空間で Evolution ゲートに分解し、それを1つ目のメソッドが処理します。ただし、ModeRotation 用に登録したプラグインを使えば、代わりにゲートを直接ロワリングできます。ボソンモードを回転させると分解せずに回転をロワリングするを参照してください。メソッドは、B2QSynthesis の config 引数で名前を指定して登録するか、これまでの例のように B2QSynthesis.methods にプラグインオブジェクトを直接書き込むことで追加できます。
量子ビットレベルで続行する
プリセットは常に qubit ステージで終わります。これは Qiskit 自身のプリセットパスマネージャーです。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']
他にも一般的な選択肢が2つあります。上記の例のように、パイプラインが合成した回路をそのまま表示したい場合は、空の PassManager を qubit ステージに割り当てます。あるいは、パイプラインを自分で組み立て、任意の Qiskit パスマネージャーを MultiStagePassManager の qubit ステージとして追加します。ステージは指定された順序で実行されます。Qiskit のパスは完成した回路ではなく DAG を想定しているため、qubit ステージは output より前に置く必要があります。
>>> 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 ゲートを合成する: Evolution ゲートに対する積公式の選び方について説明します。
-
ボソンモードを回転させる: モード回転のロワリングについて説明します。
-
エンコーディングを選択してボース・ハバード・ダイナミクスをシミュレーションする: ロワリング後の回路を厳密なダイナミクスと照合する、完全なシミュレーションを紹介します。
-
qiskit_bosons.transpiler: ステージと利用可能なパスの詳細な説明です。