演算子表現のマッピング
マッパーは、BosonOperator を量子ビット演算子、ここでは SparseObservable に変換します。演算子自体は量子ビットについて何も知りません。演算子はモード上の生成・消滅演算子を格納しており、ボソンのモードは任意の数の粒子を保持できます。演算子を有限にするすべての要素(占有レベルをいくつ保持するか、そしてそれらのレベルを量子ビット上にどのように配置するか)は、マッピングの際に決定されます。
ライブラリのマッパー occupation_encoding() は占有数(Fock)基底で動作します。モードのレベル は、 個のボソンを持つ状態です。このマッパーは演算子と、切り捨てとエンコーディングを保持する LevelSpec を受け取ります。このガイドでは、これら2つの選択が何を意味し、それぞれにどのようなコストがかかるかを説明します。これらを使うエンドツーエンドの手順についてはエンコーディングを選択してBose-Hubbardダイナミクスをシミュレーションするを、トランスパイラーが同じ選択を回路全体にどのように適用するかについてはボソン回路をトランスパイルするを参照してください。
Fock空間を切り捨てる
レベルまでに保たれたモード(ここで はその max_occupation)は、もはや正準交換関係を満たしません。それらのレベルに切り捨てた消滅演算子を と書くと、
したがって、この関係は最高レベルを除くすべてのレベルで成り立ちます [1]。これは、2つの生成・消滅演算子の交換子をマッピングし、その対角成分を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]
最後のエントリーは です。これは、 の張る空間が交換子に関して閉じていないことを意味するため、どのエンコーディングも代数の同型写像にはなりません。エンコーディングが写すのは状態と、切り捨てられた部分空間の内部で作用する演算子です。次の2つの帰結が実際には重要です。
カットオフは到達可能な占有数を上から抑えなければなりません。 開始時の占有数を抑えるだけでは不十分です。 に作用するビームスプリッターは と を占有させるため、カットオフが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になります。ダイナミクスが占有させるはずの2つの状態が存在しないためです。カットオフが2の場合、その確率は消えます。これがHong-Ou-Mandel効果です。max_occupation が必須の引数でありデフォルトを持たないのはこのためです。これはモデリング上の決定であり、スイープして調べることが想定されている収束パラメーターです。
マッピングは正規順序化と可換ではありません。 演算子 と は切り捨ての前には等しいですが、異なる量子ビット演算子にマッピングされます。
>>> 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]
2つは最高レベルを除くすべてのレベルで一致します。最上位のレベルに適用すると、 には行き先がなく振幅が切り捨てられるため、そこでは はゼロになりますが、 は を与えます。ハミルトニアンが正規順序形で定義されるかどうかを一度決め、その形を一貫してマッピングしてください。
カットオフの選択に役立つ3つのメソッドがありますが、いずれも下限です。演算子は、1つの項の中で各モードに作用する生成演算子の最大数を報告し、スペックはそれを自身のカットオフと比較します。
>>> 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]
ゼロでないエントリーは、カットオフが下限を下回っていることを意味し、その値はどれだけ足りないかを表します。これらのヘルパーはダイナミクスについて何も知らないため、チェックを通過してもカットオフが十分であるとは限りません。上のビームスプリッターは下限が1ですが、カットオフ2が必要です。また、カットオフが1のときの のように、保持されたレベル上でゼロになる項についても不足を報告することがあります。
LevelSpecで切り捨てを記述する
LevelSpec は、すべてのモードのカットオフとエンコーディングをまとめ、そこから各モードが占有する量子ビット数とその開始位置を導出します。これについて一致していなければならないすべてのもの(マッパー、トランスパイラーのレイアウト、状態準備)には、同じスペックが渡されます。エンコーディングは必須の引数であり、カットオフも同様です。どちらもデフォルトを持ちません。
カットオフは、すべてのモードで共有される1つの整数か、モードごとの整数のいずれかです。
>>> 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
モード は量子ビット 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
エンコーディングが量子ビットを数える方法から、いくつかの制限が生じます。コンパクトなエンコーディングでは、レベルが1つだけのモード(max_occupation = 0)は量子ビットをまったく必要としません。そのただ1つのレベルは、他のレベルと区別するために何も必要としないためです。このようなモードは許可されており、後続のモードの量子ビットの位置を動かしませんが、すべてのモードが量子ビットを必要としなくなるスペックは拒否されます。ユナリーエンコーディングはモードあたり最大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 は、量子ビットのブロックのどの基底状態が各レベルを表すかを決定します。エンコーディングは4つあり、一般にどれか1つが最良ということはありません [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']
ビットパターンは整数で、ビット が量子ビット の値です。ユナリーエンコーディングはレベルごとに1量子ビットを使い、そのうち1つをセットします。コンパクトなエンコーディングは 個の量子ビットを使い、レベル番号を2進数、または隣接するレベルが1ビットだけ異なるGrayコードで格納します。エンコーディングの文字列名は、エンコーディングが受け付けられる場所ならどこでも受け付けられます。
トレードオフは、量子ビット数と演算子の重みの間にあります。コンパクトなエンコーディングは必要な量子ビット数が指数的に少なくなりますが、生成・消滅演算子はビットパターンが複数のビットで異なりうるレベル同士を結ぶため、その項はより多くの量子ビットに作用します。以下のセクションでは、これにどのようなコストがかかるかを、レイアウトが同一で演算子のみが異なる2つのユナリーエンコーディングから順に見ていきます。
2つのユナリーエンコーディングを比較する
ユナリーエンコーディングでは、生成・消滅演算子は1つの励起を隣接する2つの量子ビットの間で移動させます。これをモード全体に対する演算子として書くには、他の量子ビットをどう扱うかを選ぶ必要があります。2つのバリアントは異なる選択をします。
-
unary_projectedは、モードの他のすべての量子ビットに射影演算子 を置きます。これにより、演算子自体にone-hot制約が課されます。項は、モードの残りが空の場合にのみ作用します。 -
unary_unprojectedはそれらの量子ビットに手を加えないため、項はそれらに対して恒等演算として作用します。
で、両方の下での単一の を見てみましょう。マッパーは SparseObservable を返し、その項は単一量子ビットのビット項の文字列です。ここでは、それぞれの最初の4つの項を、作用する量子ビットとともに示します。
>>> 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
どちらも、同じ係数を持つ12個の項を持ちます。射影された項は、射影されていない項が触れない2つの量子ビットに 0 ビット項を持つため、射影された各項は4つすべての量子ビットに触れ、射影されていない各項は2つに触れます。一般に、射影された項は、 または のそれぞれである2つの因子と、他の各量子ビットへの射影演算子を持つため、モードの 個の量子ビットすべてに触れます。
2つの演算子は、エンコーディングが想定している状態、つまりone-hot基底状態(コードワード)上では等しく、それ以外では異なります。確認するために、 で2つのモードに作用する小さな演算子を取り、9つのコードワード上で行列を比較します。
>>> 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
どちらもコードワード部分空間を不変に保つため、コードワード上で準備された状態はコードワード上にとどまります。この選択が重要になるのは、たとえばノイズによってその部分空間から外れる状態に対してのみです。その場合、射影された演算子は射影が失敗するため時間発展を抑制しますが、射影されていない演算子は作用し続けます。どちらの挙動も物理的な要件ではありません。射影されていない演算子はone-hot部分空間上でのみ定義されるため、それを選ぶことはモデリング上の決定であり、自由な最適化ではありません。
射影演算子のコストはゲートです。1つのモードに対するエルミートな を短い時間だけ発展させ、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が発展ゲートをどのように合成するかに依存し、それはリリース間で変わるため、プロットしてみましょう。
射影されていない場合の数は、 個の各遷移が2量子ビット回転であるため、 に対して線形に増加します。射影された場合の数は、すべての項が 個の量子ビットすべてに作用するため、はるかに速く増加します。コンパクトなエンコーディングは必要な量子ビット数が少なくなりますが、その項の重みは、隣接するレベルのビットパターンがどのように異なるかに依存します。1ビットだけ異なるGrayコードは2つのコンパクトなエンコーディングのうち低コストな方であり、標準の2進数は、桁上がりが複数のビットを同時に反転させるとき()に跳ね上がります。コンパクトなエンコーディングが射影されていないユナリーエンコーディングにも勝るかどうかは、 と、Qiskitが時間発展をどのように合成するかに依存するため、ある1つのサイズでの順位ではなく傾向を読み取ってください。
射影演算子は、マッピングされた演算子をPauliに展開するときにも重要です。from_sparse_observable() は、各射影演算子を2つのPauli演算子の和に置き換えることでこれを行います。 個の射影演算子を持つ項は 個のPauli項になります。ここでは小さな での単一の を示しますが、これはこの処理を試してよい唯一のサイズです。
>>> 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
どちらのバリアントも同じ数の項を格納しますが、射影された方の展開は に対して指数的に増加します。おもちゃの例より大きなものについては、変換するのではなく、上のリストのように SparseObservable の項を直接読み取ってください。
コンパクトなエンコーディングは射影演算子を除去できません。そこではゼロビットがレベル番号の一部であるため、 を取り除くと項が結ぶレベルが変わり、演算子はコードワード上で同じではなくなります。これが、ユナリーエンコーディングだけが2つのバリアントを持つ理由です。
生成・消滅演算子の積は先に切り捨てられる
もう1つの設計上の選択は、対角項のコストに現れます。マッパーは各生成・消滅演算子をマッピングして結果を掛け合わせるのではありません。項が1つのモードに適用する生成・消滅演算子を集め、それらをFock基底の 行列として掛け合わせ、その積を一度だけ切り捨ててから、結果をエンコードします [2]。したがって、数演算子 のような積は対角のままです。
>>> 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
その項は、ゼロより上の各レベルの量子ビット上の射影演算子 で、レベル番号で重み付けされています(レベルゼロは重みがゼロなので項を持ちません)。その時間発展は単一量子ビット回転の集合であり、2量子ビットゲートを必要としません。代わりにマッピングされた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モデルのオンサイト相互作用のような対角項はこれらの積であり、この選択が重要になるのはそこです。
カスタムマッパーを書く
ライブラリが提供しないエンコーディングが必要な場合は、単一の生成・消滅演算子の像からそれを構築します。map_boson_action_generators() は、1つの BosonAction をマッピングする関数と、恒等演算を返す関数を受け取り、それらから演算子全体のマッパーを構築します。各項のアクションの像を順に掛け合わせ、係数でスケーリングし、項を足し合わせます。出力は、スカラー倍と合成をサポートする任意の型にできます。
例として、モードを1つの量子ビットに格納する のエンコーディングを取り上げます。生成演算子は で、Qiskitの規約では であり、消滅演算子はその随伴です。
>>> 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
この手書きのマッパーは、マッピングされた生成・消滅演算子を掛け合わせますが、すべてのモードが2つのレベルを持ち、量子ビットがモード全体を保持するため、ここではライブラリのマッパーと一致します。前のセクションで示したように、 が大きくなると、マッピングされた生成・消滅演算子を合成することと、先に積を切り捨てることは、コードワードの外では同じ演算子ではありません。
このようなマッパーを回路で使うには、シグネチャ (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)
知っておくべき詳細が2つあります。1つ目は、量子ビット演算子は実数の係数を持たなければならないことです。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']
2つ目は、積公式は項を受け取った順序で合成し、マッパーが生成する順序が安定していることは保証されないという点です。ヘルパー simplify() と group_wise() は、マッパー関数をラップしてそれを制御します。simplify は重複する文字列をマージし、項を正準な順序に並べ替えます。一方、group_wise は演算子の1つのグループずつをマッピングし、グループの項が隣接したままになるようにします。ソートはグループ化を破棄するため、この2つを組み合わせないでください。グループ化ガイドではグループの割り当て方法を、合成ガイドでは順序が回路にどう影響するかを示します。
次のステップ
-
カットオフをスイープし、エンコーディングを厳密なダイナミクスと比較する完全なシミュレーションについては、エンコーディングを選択してBose-Hubbardダイナミクスをシミュレーションするに従ってください。
-
同じ
LevelSpecがボソン回路にどのように適用されるかについては、ボソン回路をトランスパイルするを参照してください。 -
BosonOperatorが項をどのように格納し順序付けするかについては、上の正規順序化の例の背後にある縮約規則も含めて、演算子表現の設計原則をお読みください。 -
ボソンをモード間で移動させるゲートについては、ボソンモードを回転させるをお読みください。到達可能な占有数に関する警告が最も重要になるのはそこです。
-
qiskit_bosons.mappersとqiskit_bosons.mappers.libraryのAPIリファレンスを参照してください。
[1]
R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.
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.