量子コンピュータ向けフレームワークCirqの基礎

kenya-sk
21 min readJan 26, 2024

Cirqとは

Copy from GitHub repository of Cirq

CirqはGoogle Quantum AIチームが開発したオープンソースの量子コンピュータ向けフレームワークです。Pythonを用いて量子回路の記述、操作、最適化が可能で、作成した回路は様々な量子デバイスやシミュレータ上で実行することができます

CirqのGitHubレポジトリ:https://github.com/quantumlib/cirq

Cirqの基本機能

・Qubitの定義

qubitは、ラベルの有無や配置方法によって以下の3パターンのいずれかで定義することができます

  • cirq.NamedQubit
    - 名前を表すラベルのみをもつ抽象的なqubitとして定義
    - 多くのqubitを必要としない場合に利用推奨
  • cirq.LineQubit
    - 連続する整数値をインデックスとして、直線上に配置されるようにqubitを定義
    - 複数のqubitを簡単に定義可能
    - qubitを直線上に並べたイオントラップ形式のデバイス等での利用推奨
  • cirq.GridQubit
    - グリッド状に配置するようにインデックスを付与したqubitを定義(インデックスは0から始まり(0, 2)のようにアクセスする)
    - qubitをグリッド状に配置するデバイスでの利用推奨(Googleが提供している量子チップの多くはグリッドタイプ)
# NamedQubitの定義
q0 = cirq.NamedQubit('source')
q1 = cirq.NamedQubit('target')

print("--------- NamedQubit ---------")
print(f"q0={q0}")
print(f"q1={q1}")

# LineQubitの定義
# 特定のインデックスのqubitを作成
q3 = cirq.LineQubit(3)

print("--------- LineQubit ---------")
print(f"q3={q3}")

# 複数のqubitをまとめて作成
q0, q1, q2 = cirq.LineQubit.range(3)
print(f"q0={q0},q1={q1},q2={q2}")

# GridQubitの定義
# 特定のインデックスのqubitを作成
q4_5 = cirq.GridQubit(4, 5)

print("--------- GridQubit ---------")
print(f"q4_5={q4_5}")

# グリッド状の複数qubitをまとめて作成
# 例) 4x4: (0, 0) 〜 (3, 3)
qubits = cirq.GridQubit.square(4)
print(f"qubits={qubits}")

# 出力
# --------- NamedQubit ---------
# q0=source
# q1=target
# --------- LineQubit ---------
# q3=q(3)
# q0=q(0),q1=q(1),q2=q(2)
# --------- GridQubit ---------
# q4_5=q(4, 5)
# qubits=[cirq.GridQubit(0, 0), cirq.GridQubit(0, 1), cirq.GridQubit(0, 2), cirq.GridQubit(0, 3), cirq.GridQubit(1, 0), cirq.GridQubit(1, 1), cirq.GridQubit(1, 2), cirq.GridQubit(1, 3), cirq.GridQubit(2, 0), cirq.GridQubit(2, 1), cirq.GridQubit(2, 2), cirq.GridQubit(2, 3), cirq.GridQubit(3, 0), cirq.GridQubit(3, 1), cirq.GridQubit(3, 2), cirq.GridQubit(3, 3)]

また特定のデバイス上でqubitがどのように配置されているか知りたい場合は、以下のようにして確認できます。そのほかにも、メタデータ等の様々な情報にアクセスすることができます。詳しくは、Devicesページを参照してください

print(cirq_google.Sycamore)

# 出力
# (0, 5)───(0, 6)
# │ │
# │ │
# (1, 4)───(1, 5)───(1, 6)───(1, 7)
# │ │ │ │
# │ │ │ │
# (2, 3)───(2, 4)───(2, 5)───(2, 6)───(2, 7)───(2, 8)
# │ │ │ │ │ │
# │ │ │ │ │ │
# (3, 2)───(3, 3)───(3, 4)───(3, 5)───(3, 6)───(3, 7)───(3, 8)───(3, 9)
# │ │ │ │ │ │ │ │
# │ │ │ │ │ │ │ │
# (4, 1)───(4, 2)───(4, 3)───(4, 4)───(4, 5)───(4, 6)───(4, 7)───(4, 8)───(4, 9)
# │ │ │ │ │ │ │ │
# │ │ │ │ │ │ │ │
#(5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)───(5, 5)───(5, 6)───(5, 7)───(5, 8)
# │ │ │ │ │ │ │
# │ │ │ │ │ │ │
# (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)───(6, 6)───(6, 7)
# │ │ │ │ │
# │ │ │ │ │
# (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
# │ │ │
# │ │ │
# (8, 3)───(8, 4)───(8, 5)
# │
# │
# (9, 4)

・GateとOperationの定義

図1. GateとOperationの関係性 [4]
  • Gate
    - qubitの集合に対して適用し、状態を変化させるもの
    - 例) Hadamard Gate (cirq.H)
  • Operation
    - qubitの集合に対してGateを適用する操作
    - 例) cirq.H(cirq.LineQubit(1))
# Gateの例
cnot_gate = cirq.CNOT
sqrt_x_gate = cirq.X**0.5 # same as `cirq.XPowGate(exponent=0.5)`

print("--------- Gate ---------")
print(f"Controlled-X Gate: {cnot_gate}")
print(f"Square Root of X Gate: {sqrt_x_gate}")

# Operationの例
q0, q1 = cirq.LineQubit.range(2)
z_op = cirq.Z(q0)
not_op = cirq.CNOT(q0, q1)

print("--------- Operation ---------")
print(f"Pauli-Z Operation: {z_op}")
print(f"Controlled-X Operation: {not_op}")

# 出力
# --------- Gate ---------
# Controlled-X Gate: CNOT
# Square Root of X Gate: X**0.5
# --------- Operation ---------
# Pauli-Z Operation: Z(q(0))
# Controlled-X Operation: CNOT(q(0), q(1))

そのほかにもCirqでは、利用可能なGateが多く用意されています。また、必要に応じてカスタムGateを作成することも可能です。詳しくはGates and operationsページを参照してください

・Circuitの定義

Circuitは量子回路を表し、Momentと呼ばれる概念の集合で構成されます。Momentとは、同じ抽象的な時間軸上 (量子回路図では垂直方向の軸)でのOperationの集合を概念的に表現したものとなります (図1参照)。Cirqでは、cirq.Circuit()でインスタンスを作成できます

Cirqで作成した回路図上では、各Operationは等価性を保ちながら自動的に左側にシフトして表示されています。Momentを明示的に定義したい場合は、cirq.Moment()を活用することで対応可能です

# 3qubitに対して、H Gateを適用した回路を定義
print("--------- Default Circuit ---------")
print(cirq.Circuit(cirq.H(q) for q in cirq.LineQubit.range(3)))

# 上記と同様のcirquitに対して、明示的にmomentを定義
print("--------- Defined Moment ---------")
print(cirq.Circuit(cirq.Moment([cirq.H(q)]) for q in cirq.LineQubit.range(3)))

# 出力
# --------- Default Circuit ---------
# 0: ───H───
#
# 1: ───H───
#
# 2: ───H───
#
# --------- Defined Moment ---------
# 0: ───H───────────
#
# 1: ───────H───────
#
# 2: ───────────H───

量子デバイスでは、各qubitの結合状態等のハードウェアの制約が存在します。そのため、作成したCircuitが特定のデバイス上で実行可能かを検証し、エラーを生成するメソッドも用意されています。ここでは、例として作成したCircuitをGoogleのSycamoreプロセッサ上で実行可能かを検証してみます

# グリッド形式の3qubitを準備
q0 = cirq.GridQubit(5,6)
q1 = cirq.GridQubit(5,5)
q2 = cirq.GridQubit(4,5)

# 各qubitに対するOperationを定義
adjacent_op = cirq_google.SYC(q0,q1)
nonadjacent_op = cirq_google.SYC(q0, q2)

# 対象デバイスで実行可能なcircuitを定義
working_circuit = cirq.Circuit()
working_circuit.append(adjacent_op)
valid = cirq_google.Sycamore.validate_circuit(working_circuit)

# 対象デバイスで実行不能なcircuitを定義
bad_circuit = cirq.Circuit()
bad_circuit.append(nonadjacent_op)

# 今回の例では、q0とq1がデバイス上でペアとして接続されていないが
# Sycamore gateが適用されているためエラーが出る想定
try:
cirq_google.Sycamore.validate_circuit(bad_circuit)
except ValueError as e:
print(e)

# 出力
# Qubit pair is not valid on device: (cirq.GridQubit(5, 6), cirq.GridQubit(4, 5)).

・Simulatorの利用

cirq.Simulator()インスタンスを利用することで、作成したCircuitのシミュレーションを実行することができます(023/01時点では20qubitsまで)

シミュレータの利用方法には以下の2パターンがあります

  • simulate()
    - 古典的にシミュレーションを行うことができ、波動関数を確認することも可能
    - デバックや回路の挙動を確認したい場合に利用
  • run()
    - 実際の量子デバイスを使用した場合に近く、最終的な結果しか得ることができない
    - 結果の分布を得るために、repetitionsパラメータでサンプル数を指定し、シミュレータをサンプラーとして実行することで、ビット列を得る
# 2qubitがエンタングルされたベル状態の回路を作成
# 1/sqrt(2) * ( |00⟩ + |11⟩ )
bell_circuit = cirq.Circuit()
q0, q1 = cirq.LineQubit.range(2)
bell_circuit.append(cirq.H(q0))
bell_circuit.append(cirq.CNOT(q0, q1))

# シミュレータを初期化
s = cirq.Simulator()

print("--------- Simulated by `simulate` method ---------")
results = s.simulate(bell_circuit)
print(results)

bell_circuit.append(cirq.measure(q0, q1, key="result"))
samples = s.run(bell_circuit, repetitions=100)
# samples[0]はq0に、samples[1]はq1に対応するサンプル数分の観測結果を格納
print("--------- Simulated by `run` method ---------")
print(samples)

# 出力
# --------- Simulated by `simulate` method ---------
# measurements: (no measurements)
#
# qubits: (cirq.LineQubit(0), cirq.LineQubit(1))
# output vector: 0.707|00⟩ + 0.707|11⟩
#
# phase:
# output vector: |⟩
#
# --------- Simulated by `run` method ---------
# result=1001101001010001110010010100100101110101100110100001101111101100010000000000111101000100011010001000, 1001101001010001110010010100100101110101100110100001101111101100010000000000111101000100011010001000

NISQのようなノイズの影響を考慮した環境でシミュレーションを行いたい場合には、Quantum Virtual Machine (QVM)を利用することができます。詳しくはQuantum Virtual Machineのページを参照してください

・結果の可視化

runメソッドを利用してシミュレーションを実行した場合には、得られた観測結果のヒストグラムを作成するcirq.plot_state_histogramメソッドが用意されています

import matplotlib.pyplot as plt

# 前セクションで得られたsampleを可視化
cirq.plot_state_histogram(samples, plt.subplot())
plt.show()
図2. シミュレーションの観測結果 (全qubit)

上記の実行方法では、全qubitについてのヒストグラムが作成されます。そのため、qubit数が増えてくるとスパースなグラフとになり、視認性が下がってしまいます。そのような場合には、以下のように観測された結果に絞り込んで可視化する方法を活用します

# 観測された結果についてのみカウント
counts = samples.histogram(key='result')
print(counts)

# 観測された結果のみのヒストグラムを作成
cirq.plot_state_histogram(counts, plt.subplot())
plt.show()
図2. シミュレーションの観測結果 (観測された結果のみ)

ヒストグラムを作成する際の集計軸や並び順は、Resultクラスのメソッドであるhistogramを呼ぶ際の引数fold_funcで制御することができます。また、グラフのタイトルや軸ラベル等も指定することが可能です。実際の使用例はこちらの公式ドキュメントを参照してください。

・回路の変換

  • ユニタリ行列表現
    多くのOperationやCircuitは、ユニタリ行列表現で表すことができ、cirq.unitary(operation or circuit)を実行することで行列を得ることができます
print("--------- Unitary of X gate ---------")
print(cirq.unitary(cirq.X))

print("--------- Unitary of Circuit ---------")
q0, q1 = cirq.LineQubit.range(2)
print(cirq.unitary(cirq.Circuit(cirq.X(q0), cirq.SWAP(q0, q1))))

# 出力
# --------- Unitary of X gate ---------
# [[0.+0.j 1.+0.j]
# [1.+0.j 0.+0.j]]
#
# --------- Unitary of Circuit ---------
# [[0.+0.j 0.+0.j 1.+0.j 0.+0.j]
# [1.+0.j 0.+0.j 0.+0.j 0.+0.j]
# [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
# [0.+0.j 1.+0.j 0.+0.j 0.+0.j]]
  • Decompositions
    多くのGateで構成されたCircuitをより単純なオペレーションとゲートで構成した等価な回路に変換するcirq.decomposeメソッドも用意されています。単純化することで、多くのデバイスでサポートされていない3qubitを利用するゲートを実行可能な1または2qubitの組み合わせに変換することもできます
# Hadmard GateをX GateとY Gateの組み合わせに変換
print(cirq.decompose(cirq.H(cirq.LineQubit(0))))

# 出力
# [(cirq.Y**0.5).on(cirq.LineQubit(0)), cirq.XPowGate(global_shift=-0.25).on(cirq.LineQubit(0))]
  • Transformers
    Circuitを受け取り、より効率的で短いオペレーションの組み合わせに変換を行います(理論的にはあらゆる種類の回路操作が可能)。変換は基本的に以下のステップで行われます
    1. Gate Decompositions: デバイスで実行可能なGateだけを用いてCircuitを変更
    2. Qubit Mapping and Routing: 回路の論理qubitをデバイスの物理qubitにマッピングし、デバイスのトポロジーを尊重するようにqubitのスワップ操作を挿入
    3. Circuit Optimizations: デバイス固有の最適化を実行
# 変換前の回路
q = cirq.GridQubit(1, 1)
c = cirq.Circuit(cirq.X(q)**0.25, cirq.Y(q)**0.25, cirq.Z(q)**0.25)
print("--------- Before Transform ---------")
print(c)

# 変換後の回路
c = cirq.merge_single_qubit_gates_to_phxz(c)
print("--------- After Transform ---------")
print(c)

# 出力
# --------- Before Transform ---------
# (1, 1): ───X^0.25───Y^0.25───T───
#
# --------- After Transform ---------
# (1, 1): ───PhXZ(a=0.304,x=0.333,z=0.142)───

ビルドインのTransformerは、ドキュメントにまとまっているので必要に応じて参照してください。また、カスタムTransformerを定義することも可能となっています。作成方法については、こちらのドキュメントを参照してください

Best Practices

Cirqライブラリに関するベストプラクティスも提供されています。ここで述べられていることはCirqに限らず他のライブラリ利用時にも適用できることが記載されています。ベストプラクティスを参考にすることで、よりパフォーマンスが高く、バージョン間でロバストなコードとなることが期待できます

  • クラスをトップレベルで利用する
    Cirqではユーザ向けの重要なクラスはパッケージレベルで公開されています。そのため、モジュールレベル (cirq.ops.Xなど)ではなく、パッケージレベル (cirq.Xなど)で参照することが推奨されています。そうすることで、モジュール名の変更やバージョン間での移動に対してロバストになります
  • immutableクラスを変更しない
    cirq.Gate、cirq.Operation、cirq.Moment、cirq.ParamResolverはimmutableなオブジェクトなため作成後に変更をしない。変更が必要な場合は、新しいオブジェクトとして作成する
  • 計算量の指数増加を意識する
    量子コンピュータで扱うアルゴリズムの多くは計算量が指数関数的にスケールする。Cirqは現状NISQ領域向けに設計されているため、数百〜数千量子ビット規模の回路を作成する際は注意する必要がある。たとえ、少ない量子ビットでも、計算量が爆発的に増えることがあるので常に意識をする
  • 開発者の意図を明確にする
    Cirqは、コードで定義された回路になるべく忠実に実行しようと試みます。そのため、開発者の意図しない回路の自動分解、コンパイル、その他の変換を推奨していません。回路の修正や変換は開発者が明示的にやるべきという設計になっています。そのために、Cirqには回路のコンパイルと変換のための関数が用意されています。これらを積極的に活用して、開発者の意図を回路に組み込みます
  • その他のTips
    - cirq.CircuitOperationクラスを利用すると繰り返しの多い大規模な回路をよりコンパクトに定義することが可能となります
    - 複数の回路を実行する場合は、run_sweepやrun_batchを利用する。そうすることで、ハードウェア側で実行を最適化する機会が増え、実行速度が大幅に向上します
    - より視認性の高いコードを作成するために、コードや関数の最初に量子ビットを定義して、その後ゲートや回路を適用します

References

  1. https://quantumai.google/cirq/start/basics
  2. https://quantumai.google/cirq/build/qubits
  3. https://quantumai.google/cirq/hardware/devices
  4. https://quantumai.google/cirq/build/gates
  5. https://quantumai.google/cirq/simulate/quantum_virtual_machine
  6. https://quantumai.google/cirq/transform/transformers
  7. https://quantumai.google/cirq/transform/custom_transformers
  8. https://quantumai.google/cirq/start/best_practices

--

--