メインコンテンツへスキップ
このプロジェクトはベータ版です。バグを見つけましたか?GitHub Issueを開く。質問やアイデアがありますか?Discussionsで質問する — フィードバックをお待ちしています!
このページの翻訳品質はいかがですか?

オペレーター表現のマッピング

マッパーは、数学的な等価性を保ったまま、オペレーターをある表現から別の表現へ変換するルーチンです。さまざまなオペレーター代数にまたがって柔軟に作業したり、量子回路の実行に向けてオペレーターを準備したりするために使います。

カスタム・マッパーの実装​

マッパー・モジュールを使うと、カスタムのマッピングを簡単に実装できます。マッパーは、オペレーターを構成する基本的なアクション(生成、消滅、マヨラナなど)を変換し、変換後のアクションをターゲット表現の規則に従って組み合わせることで動作します。

具体例として、map_majorana_action_generators() を使って MajoranaOperator を量子ビット演算子に写像するJordan-Wigner変換を考えてみましょう。

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>

カスタムマッパーを実装するための主な手順は次のとおりです。

  1. アクション変換を定義する: 各アクション(モードでインデックス付けされたもの)が対象の表現へどのように写像されるかを指定します。上の例では、MajoranaアクションはJordan-Wignerの規則に従ってPauli文字列に写像されます。

  2. 提供されているマッピング関数のいずれかを使う: mappers モジュールには、operators モジュールが提供する演算子表現について、演算子の項を反復処理し、カスタムのアクションマップを順に適用するためのユーティリティ関数が用意されています。たとえば、map_majorana_action_generators() はMajorana演算子に対応しています。

  3. 結果を取得する: マッピング関数は、すべての項に一貫して変換を適用した、対象の表現の演算子を返します。

この設計により、演算子の内部構造に深い知識がなくても、カスタムマッピングを簡単にプロトタイピングできます。変換は演算子のすべての項に自動的に適用され、数学的な等価性が保たれます。

ヒント

用意されているイテレーター関数を使わずに、演算子の項を手動で反復処理することもできます。詳細は、演算子ガイドの項の反復処理のセクションを参照してください。

ライブラリ実装​

qiskit_fermions.mappers.library モジュールは、一般的なマッピングについて、効率的で十分にテストされた実装を提供します。これらはカスタムマッパーと同じ基本パターンに従いますが、本番利用向けに最適化されており、Python APIとC APIの両方で利用できます。

以下の例では、カスタムマッパーのセクションと同じ MajoranaOperator にライブラリのマッパーを適用し、同等の結果が得られることを示します。

>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
注

上の2段階の手順は、2つのライブラリマッパーを組み合わせる様子を示すために書き出したものです。実際には、MajoranaOperator は majorana_jordan_wigner()(C APIでは qf_maj_op_jordan_wigner())を使って1ステップで写像します。他の演算子タイプについても同様で、edge_vertex_jordan_wigner() と transfer_vertex_jordan_wigner()(qf_edge_op_jordan_wigner()、qf_transfer_op_jordan_wigner())があります。FermionOperator を経由すると中間結果も膨らみます。これらの代数のすべての生成子は単一のPauli文字列に写像されるのに対し、各フェルミオンアクションは2項の和に写像されるため、複数の生成子から成る項は展開されたのち、再び統合されることになります。この節約効果は項が長くなるほど大きくなり、ホッピングハミルトニアンのような単一生成子の項では、2つの経路のコストはほぼ同じです。

Python APIでは、これらのライブラリマッパーの一部は、型に依存しない便利なマッパーからも利用できます。fermion_operator() と majorana_operator() は、majorana_to_fermion() や fermion_to_majorana()(ほか)の前段に位置し、渡されたオブジェクトの型に基づいて内部で適切なライブラリ実装に処理を委ねるため、適切な関数を自分で選ぶ必要がありません。同じことが jordan_wigner() と(後述の)ternary_tree() にも当てはまり、それぞれ、渡された演算子に対応する直接の実装へ振り分けます。この振り分けが内部でどのように実装されているかについては、qiskit_fermions.protocols を参照してください。

三分木でエンコーディングを選ぶ​

Jordan-Wignerはフェルミオンから量子ビットへの唯一のエンコーディングではなく、最も低コストというわけでもありません。その ZZ 文字列はモードのインデックスとともに長くなるため、nn 個のモードの最後にあるMajorana演算子は、重み nn のPauli文字列に写像されます。パリティ情報を鎖ではなく木に分散させるエンコーディングは、漸近的により優れた性能を示します。

nn 個のノードをもつ三分木は、この一族全体を捉えます。各ノードは X、Y、Z とラベル付けされた3本の下向きのリンクをもつ量子ビットです。リンクは子ノードへ降りるか(辺)、終端になるか(葉)のいずれかです。各葉からルートへ戻る経路はPauli文字列を表します。ノード uu から降りるラベル PP のリンクをたどると、量子ビット uu 上の PP が寄与します。どの木も有効なエンコーディングを与え、よく知られたものは特定の形にすぎません。Z に沿った鎖はJordan-Wignerであり、ノード数が許す限りすべてのノードが3つの子をもつ木は平衡木です。

>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3

この最大重みは最適です。Jiang et al. は、nn 個のモードに対するあらゆるフェルミオンから量子ビットへのマッピングのMajorana演算子は、平均Pauli重みが少なくとも log⁡3(2n)\log_3(2n) であることを証明しており、したがって最大のものを ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil 未満にできるマッピングは存在しません。どちらの木もちょうど nn 個の量子ビットを使うため、節約されるのは量子ビット数ではなくPauli重みです。

演算子を写像するには、木をエンコーディングにコンパイルし、結果の量子ビット数とともにマッパーに渡します。コンパイルでは、木の葉をMajorana演算子として対にし、それらのPauli文字列を一度だけ構築するため、同じエンコーディングを多数の演算子で再利用してください。量子ビット数はエンコーディングではなくマッピング呼び出し側で指定するため、1つのエンコーディングで異なる幅のレジスタに対応できます。木のノード数を超える量子ビット数を指定した場合は、恒等演算子で埋められます。

>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>

Z に沿った鎖を通したマッピングは、fermion_jordan_wigner()(C APIでは qf_ferm_op_jordan_wigner())をそのまま再現します。これが、このエンコーディングがJordan-Wignerを一般化しているという意味です。

>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0

すべての演算子表現には、Jordan-Wignerのセットに対応する三分木マッパーがあります。完全な一覧は qiskit_fermions.mappers.library を、C APIについては Mapper Library を参照してください。Python APIでは、jordan_wigner() と同様に、渡された演算子に適したものを選ぶ型非依存のラッパーである ternary_tree() からも利用できます。

>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
注

Jordan-Wignerで成り立つ恒等式の中には、三分木一般ではなく鎖に固有の性質であるものがあります。数演算子が nj=(1−Zj)/2n_j = (1 - Z_j)/2 となるのは鎖の場合だけで、他の木では ZjZ_j は量子ビットの集合にわたる ZZ 文字列になります。同様に、頂点演算子 VjV_j が重み1のPauli ZjZ_j になるのも鎖の場合だけで、そこでは2つのMajorana文字列の ZZ 鎖が打ち消し合います。

すべての数演算子は依然として単一の ZZ 文字列に写像されるため、どの三分木でもFock状態は計算基底状態のままですが、ビット qq はもはや占有数 qq を表しません。占有数 jj は、代わりに njn_j の像の台に含まれる測定ビットのパリティになります。デコードがどんな形やモードの置換に対しても正しく保たれるよう、これらの台は木から導出するのではなく、マッパーから読み取ってください。

>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>

この平衡木では、量子ビット0、1、2に含まれる1の数が奇数のときにモード0が占有されます。したがって、これを準備するには量子ビット0だけを反転させればよい一方、測定したビット列をデコードするには3つすべてのパリティを取る必要があります。量子ビット1での単一のビット反転エラーは、モード0とモード1の両方の占有数を壊すため、ノイズのあるサンプルから占有数を読み取るワークフローでは問題になります。

Evolution をこのようなマッパーで下位化する場合は、マッパーを group_wise() でラップしてください(フェルミオン空間でのEvolutionゲートの合成を参照)。ホッピング項は2つの可換なPauli文字列に写像され、どちらも単独では粒子数を保存せず、保存されるのは進化におけるそれらの積だけです。Jordan-Wignerでは両方の文字列が同じ量子ビットに作用するため、簡約された項の順序によって両者が隣り合ったままになります。他の木では(前述の Y_3 Z_1 Y_0 と X_3 Z_2 X_0 のように)台が異なり、元の順序でも簡約後の順序でも他の項の像がそれらの間に挟まるため、量子ビット側の積公式では粒子数が漏れ出してしまいます。

任意の木を構築する​

鎖と幅優先のコンストラクターは一様な族を扱いますが、一般的なインターフェースでは、ノードごとに1組の (parent, label) を受け取り、ルートには None を指定します(C APIではルートのインデックスを受け取ります)。探す価値のある木は、たいてい鎖でも一様でもないため、これは重要です。たとえばBonsaiアルゴリズムは、デバイスの結合グラフの全域木を成長させるため、その分岐はその接続性に従います。

>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}

葉の数に注目してください。形に関わらず、常に 2n+12n+1 です。子の少ないノードが生成するPauli文字列が少なくなるわけではありません。使われないリンクは葉になり、葉は辺の子孫と同じようにPauli文字列を表します。これら 2n+12n+1 個の文字列のうち、2n2n 個は nn 個のモードのMajorana演算子として対になり、ちょうど1つが余ります。

全パリティ​

その余った葉が、全フェルミオンパリティ ∏j(1−2nj)\prod_j (1 - 2 n_j) です。これは演算子のマッピングではなくエンコーディングの性質であるため、エンコーディング自体のメソッドになっています。

>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>

平衡木の場合が示すように、これは一般にすべてが ZZ の文字列ではありません。すべてが ZZ の経路でたどり着く葉であり、その経路上にないすべてのノードでは恒等演算子として作用します。

重要

ここでの全パリティは、課す制約ではなく、測定する観測量です。2つの固有値はどちらも物理的であるため、状態を +1+1 セクターに射影しないでください。三分木エンコーディングはモードごとに量子ビットを1つ使い、2n2^n 次元の空間全体への全射であるため、射影して取り除くべき非物理的な部分空間は残りません。

実例: Bravyi-Kitaev木​

実際のエンコーディングの木は手で書き出せるほど小さいことがまれなため、仕様は手入力するよりも計算で求めることのほうが多くなります。Bravyi-Kitaevは良い例です。この族に属していますが、その木は一様ではないため、便利なコンストラクターではカバーできません。

その木は、モードごとに1つずつあるノードに対する再帰的な分割によって構築されます。ノードの範囲 [l,r][l, r] の中で、収まる最大の2の冪を hh とすると、その範囲に置かれるノードはそのブロックの最後である l+h−1l + h - 1 です。その X リンクは、手前のブロックから構築された木へ降り、Z リンクは、後ろに残った部分から構築された木へ降り、Y リンクは葉として残ります。

>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)

4モードでは範囲全体が1つのブロックなので、最初に置かれるノードは3であり、その前に [0,2][0, 2] があり、後ろには何もありません。この部分範囲からは、単一のノード0と2の上にノード1が置かれます。木をコンパイルすると、このエンコーディングが同じ形のクラスの木にとどまらず、確かにBravyi-Kitaevであることが確認できます。最初のMajorana像は X0X1X3X_0 X_1 X_3 です。

>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]

その全パリティは、前のセクションの要点を具体的に示しています。余った葉はすべてが ZZ の経路でたどり着く葉であり、ここではその経路がルートで止まるため、観測量は4つの量子ビットすべてにわたる文字列ではなく、単一量子ビットの Z3Z_3 になります。

>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>

規模を大きくするのに必要なのは、より大きな引数だけです。どのノードも Y リンクを辺に使わないため、各ノードは最大で2方向にしか分岐せず、Pauli重みは線形ではなく対数的に増加します。

>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]