メインコンテンツまでスキップ

演算子

このチュートリアルでは、量子力学で演算子を表すOperatorPauliLabelの2つのオブジェクトについて紹介します。この二つを使ってさまざまなオブザーバブルを構築することができます。QURI Partsでは、扱う演算子はパウリ文字列で構成されています。

PauliLabel

パウリ文字列は量子計算の世界ではどこにでも存在します。QURI PartsではPauliLabelによって表現されます。このセクションではPauliLabelは何で作られているか、またどのように作成するのかを説明します。まずは基本的な構成要素、パウリ行列から始めます。

パウリ行列

QURI Partsでは、パウリ行列はEnum:SinglePauliによって表現されます。これらは、直接計算に使われるオブジェクトではなく、単にPauliLabelがどのようなパウリ行列を保持しているかを示すラベルです。

from quri_parts.core.operator import SinglePauli

assert SinglePauli.X == 1
assert SinglePauli.Y == 2
assert SinglePauli.Z == 3

パウリ文字列

前のとおり、パウリ文字列はPauliLabelによって表現されます。インターフェースとどのように作るかについて紹介します。

インターフェース

QURI Partsでは、PauliLabelはパウリ文字列を表現しています。具体的にいうと、tuple[qubit_index, SinglePauli]frozensetです。

class PauliLabel(frozenset(tuple[int, int])):
"""First int represents the qubit index and
the second represents a `SinglePauli`
"""
...

PauliLabelを作成

PauliLabelを作るには様々な方法があります。ここでは、pauli_label関数を使用した1番簡単な方法を紹介します。基本的な使い方として、pauli_label関数は2種類の入力タイプを受けつけます:

  1. パウリ文字列のようなstr
  2. (量子ビットのインデックス、SinglePauli)のペアのシーケンス
strを使用して作成

人間が読みやすいパウリ文字列のようなstrを渡すことでPauliLabelを作成することができます。

from quri_parts.core.operator import PauliLabel, pauli_label

print("Create without spacing:", pauli_label("X0 Y1 Z2 Z3 X4 Y5"))
print("Create with spacing: ", pauli_label("X 0 Y 1 Z 2 Z 3 X 4 Y 5"))
#output
Create without spacing: X0 Y1 Z2 Z3 X4 Y5
Create with spacing: X0 Y1 Z2 Z3 X4 Y5

パウリ行列の順序は問題ではないことに注意してください。

print("Create with X0 Y1 Z2:", pauli_label("X0 Y1 Z2"))
print("Create with X0 Z2 Y1:", pauli_label("X0 Z2 Y1"))
print(pauli_label("X0 Y1 Z2") == pauli_label("X0 Z2 Y1"))
#output
Create with X0 Y1 Z2: X0 Y1 Z2
Create with X0 Z2 Y1: X0 Y1 Z2
True
(qubit_index, SinglePauli)のシーケンスを作成

(qubit_index, SinglePauli)のシーケンスをpauli_label関数に渡すことでもPauliLabelを作成することができます。

print(pauli_label([(0, SinglePauli.X), (1, SinglePauli.Z)]))
print(pauli_label(zip((0, 1), (SinglePauli.X, SinglePauli.Z))))
#output
X0 Z1
X0 Z1

PAULI_IDENTITYは特別なPauliLabelです。これは、恒等演算子を表し、エントリーのないPauliLabelです。

from quri_parts.core.operator import PAULI_IDENTITY

# PauliLabel() represents an empty `frozenset`.
print(PauliLabel() == PAULI_IDENTITY)
#output
True

PauliLabelが提供するメソッド

PauliLabelはそれ自身に関する情報をもたらすいくつかのメソッドを提供します。

  • index_and_pauli_id_list: (list[qubit index], list[SinglePauli])のタプルを返すプロパティ
pauli_label("X0 Y1 Z2").index_and_pauli_id_list
#output
([0, 1, 2], [<SinglePauli.X: 1>, <SinglePauli.Y: 2>, <SinglePauli.Z: 3>])
  • qubit_indices: PauliLabelが作用する量子ビットのリスト
pauli_label("X0 Y1 Z2").qubit_indices()
#output
[0, 1, 2]
  • pauli_at: 特定の量子ビット上のパウリ行列。特定の量子インデックス上の演算子が恒等演算子ならば、Noneを返す。
print(pauli_label("X0 Y1 Z2").pauli_at(0))
print(pauli_label("X0 Y1 Z2").pauli_at(1))
print(pauli_label("X0 Y1 Z2").pauli_at(2))
print(pauli_label("X0 Y1 Z2").pauli_at(3))
#output
SinglePauli.X
SinglePauli.Y
SinglePauli.Z
None

演算子

ここでは、Operatorオブジェクトを紹介します。OperatorオブジェクトはPauliLabelの複素線形結合を表します。

インターフェース

QURI Partsでは、PauliLabelをキー、複素数の係数をバリューとする辞書として実装されています。したがって、辞書を使用してOperatorを作成することができます。

from quri_parts.core.operator import Operator

op = Operator(
{
PAULI_IDENTITY: 8 + 1j,
pauli_label("X0 Y2"): -3
}
)

print(op)
#output
(8+1j)*I + -3*X0 Y2

Operatorの演算

Operatorに項を追加するには、add_termメソッドを使用します。追加したいPauliLabelがすでにOperatorに存在する場合、その係数が更新されます。更新後の係数が0である場合、PauliLabelOperatorから削除されます。

op = Operator(
{
PAULI_IDENTITY: 8 + 1j,
pauli_label("X0 Y2"): -3
}
)

print(op, "\n")

# Add a new term to the Operator
pl, coeff = pauli_label("Y0 Z3"), 10+1j
print(f"Add {coeff} * {pl}:")
op.add_term(pl, coeff)
print(op, "\n")

# Add a `PauliLabel` that already exists in `Operator` to update the coefficient
pl, coeff = PAULI_IDENTITY, -6
print(f"Add {coeff} * {pl}:")
op.add_term(pl, coeff)
print(op, "\n")

# Add a `PauliLabel` that already exists in `Operator` to cancel the term
pl, coeff = pauli_label("X0 Y2"), 3
print(f"Add {coeff} * {pl}:")
op.add_term(pl, coeff)
print(op)
#output
(8+1j)*I + -3*X0 Y2

Add (10+1j) * Y0 Z3:
(8+1j)*I + -3*X0 Y2 + (10+1j)*Y0 Z3

Add -6 * I:
(2+1j)*I + -3*X0 Y2 + (10+1j)*Y0 Z3

Add 3 * X0 Y2:
(2+1j)*I + (10+1j)*Y0 Z3

2種類のプロパティが存在します:

  • n_terms: Operatorの項の個数
  • constant: OperatorPAULI_IDENTITYの係数を返します。PAULI_IDENTITYが存在しない場合は0を返します。
op = Operator(
{
PAULI_IDENTITY: 8 + 1j,
pauli_label("X0 Y2"): -3
}
)


print("n_terms:", op.n_terms)
print("constant:", op.constant)
#output
n_terms: 2
constant: (8+1j)

Operatorオブジェクトも基本的な算術のための複数のメソッドを提供しています。

op1 =  Operator({pauli_label("X0 Z1"): 8j})
op2 = Operator({pauli_label("Y1"): -4})

print("op1 = ", op1)
print("op2 = ", op2)

# Addition
print("")
print("Addition:")
print("op1 + op2", "=", op1 + op2)

# Subtraction
print("")
print("Subtraction:")
print("op1 - op2", "=", op1 - op2)

# Scalar Multiplication
print("")
print("Scalar Multiplication:")
print("op1 * 3j", "=", op1 * 3j)

# Scalar Division
print("")
print("Scalar Division:")
print("op1 / 2j", "=", op1 / 2j)

# Operator Multiplication
print("")
print("Operator Multiplication:")
print("op1 * op2", "=", op1 * op2)
print("op2 * op1", "=", op2 * op1)

# Hermitian conjugation
print("")
print("Hermition Conjgation:")
print("op1^†", "=", op1.hermitian_conjugated())
print("op2^†", "=", op2.hermitian_conjugated())
#output
op1 = 8j*X0 Z1
op2 = -4*Y1

Addition:
op1 + op2 = 8j*X0 Z1 + -4*Y1

Subtraction:
op1 - op2 = 8j*X0 Z1 + 4*Y1

Scalar Multiplication:
op1 * 3j = (-24+0j)*X0 Z1

Scalar Division:
op1 / 2j = (4+0j)*X0 Z1

Operator Multiplication:
op1 * op2 = (-32+0j)*X0 X1
op2 * op1 = (32+0j)*X0 X1

Hermition Conjgation:
op1^† = -8j*X0 Z1
op2^† = -4*Y1

特別なOperator、0演算子を表すzero()も存在します。これは空の辞書によって作成されたOperatorです。

from quri_parts.core.operator import zero

zero_operator = zero()
print(zero_operator == Operator())
#output
True

ヘルパー関数

その他にも、Operatorにいくつかの演算機能を提供する様々なヘルパー関数があります。ここではそれらを紹介します:

  • is_hermitian
  • commutator
  • truncate
  • is_ops_close
  • get_sparse_matrix

これらの例を以下で説明します:

is_hermitian

is_hermitianOperatorがエルミートであるかどうかをチェックします。

from quri_parts.core.operator import is_hermitian

op = Operator({pauli_label("X0"): 1})
print(f"{op} is hermitian:", is_hermitian(op))

op = Operator({pauli_label("X0"): 1j})
print(f"{op} is hermitian:", is_hermitian(op))
#output
1*X0 is hermitian: True
1j*X0 is hermitian: False

commutator

commutatorは2つの演算子の交換子を計算します。

from quri_parts.core.operator import commutator

op1 = Operator({pauli_label("X0"): 1})
op2 = Operator({pauli_label("Y0"): 1})
print(f"[{op1}, {op2}]", "=", commutator(op1, op2))
#output
[1*X0, 1*Y0] = 2j*Z0

truncate

対応している係数が非常に小さい場合、truncateOperatorからPauliLabelを削除します。(デフォルトの許容範囲は1e-8です)

from quri_parts.core.operator import truncate
op = Operator(
{
pauli_label("Z0 Y1"): 1e-6,
pauli_label("X0 Y1"): 1e-10,
pauli_label("X0 Y2"): 1,
}
)

print("original operator:\n", op)
print("truncated operator:\n", truncate(op))
print("truncated operator with tolerance = 1e-5:\n", truncate(op, 1e-5))
#output
original operator:
1e-06*Z0 Y1 + 1e-10*X0 Y1 + 1*X0 Y2
truncated operator:
1e-06*Z0 Y1 + 1*X0 Y2
truncated operator with tolerance = 1e-5:
1*X0 Y2

is_ops_close

is_ops_closeは2つの演算子が許容範囲内で近いかどうかをチェックします。第1引数と第2引数に入力されたOperatorについて、それらが許容できる誤差の範囲内で等しいかどうかを返します。

from quri_parts.core.operator import is_ops_close
op1 = Operator({pauli_label("X0 Y2"): 1})
op2 = Operator(
{
pauli_label("Z0 Y1"): 1e-6,
pauli_label("X0 Y1"): 1e-10,
pauli_label("X0 Y2"): 1,
}
)

print("op1", "=", op1)
print("op2", "=", op2)
print(f"op1 is close to op2 (atol=0):", is_ops_close(op1, op2))
print(f"op1 is close to op2 (atol=1e-5):", is_ops_close(op1, op2, atol=1e-5))
print(f"op1 is close to op2 (atol=1e-8):", is_ops_close(op1, op2, atol=1e-8))
#output
op1 = 1*X0 Y2
op2 = 1e-06*Z0 Y1 + 1e-10*X0 Y1 + 1*X0 Y2
op1 is close to op2 (atol=0): False
op1 is close to op2 (atol=1e-5): True
op1 is close to op2 (atol=1e-8): False

get_sparse_matrix

get_sparse_matrixは、OperatorPauliLabelscipy.sparse.spmatrixに変換します。

from quri_parts.core.operator import get_sparse_matrix
op = Operator({
PAULI_IDENTITY: -8j,
pauli_label("X0 Y1"): 1
})

print(get_sparse_matrix(op))
#output
(0, 0) -8j
(3, 0) 1j
(1, 1) -8j
(2, 1) 1j
(1, 2) -1j
(2, 2) -8j
(0, 3) -1j
(3, 3) -8j

明示的な行列表現を得ることもできます。

get_sparse_matrix(op).toarray()
#output
array([[0.-8.j, 0.+0.j, 0.+0.j, 0.-1.j],
[0.+0.j, 0.-8.j, 0.-1.j, 0.+0.j],
[0.+0.j, 0.+1.j, 0.-8.j, 0.+0.j],
[0.+1.j, 0.+0.j, 0.+0.j, 0.-8.j]])

OperatorPauliLabelの非自明なパウリ行列を持つ最大の量子ビットが、それが作用する状態の量子ビット数よりも小さい場合がよくあります。この場合、n_qubitオプションで演算子が作用する状態の量子ビット数を設定することができます。

get_sparse_matrix(op, n_qubits=3).toarray()
#output
array([[0.-8.j, 0.+0.j, 0.+0.j, 0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.-8.j, 0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+1.j, 0.-8.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+1.j, 0.+0.j, 0.+0.j, 0.-8.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.-8.j, 0.+0.j, 0.+0.j, 0.-1.j],
[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.-8.j, 0.-1.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+1.j, 0.-8.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j, 0.-8.j]])