物理系を表現する#

量子コンピュータの並列性を利用した計算の代表例として、量子系のダイナミクスシミュレーションについて学びます。

量子系のダイナミクスとは#

量子力学について少しでも聞いたことのある方は、量子力学の根幹にシュレーディンガー方程式というものが存在することを知っているかと思います。この方程式は

it|ψ(t)=H|ψ(t)

などと表現され、時刻tのある系の状態|ψ(t)の時間微分(左辺)が|ψ(t)へのハミルトニアンという演算子の作用で定まる(右辺)ということを表しています。ただしこの「微分形」の方程式は我々の目的には少し使いづらいので、ここでは等価な「積分形」にして

|ψ(t1)=T[exp(it0t1Hdt)]|ψ(t0)

と書いておきます。T[]は「時間順序演算子」と呼ばれ重要な役割を持ちますが、説明を割愛し、以下ではハミルトニアンHが直接時間に依存しない場合の

|ψ(t1)=exp(iH(t1t0))|ψ(t0)

のみを考えます。量子状態に対する演算子(線形演算子)の指数関数もまた演算子なので、積分形のシュレーディンガー方程式は「ei/H(t1t0)という演算子が系を時刻t0の初期状態|ψ(t0)から時刻t1の状態|ψ(t1)に発展させる」と読めます。さらに、定義上ハミルトニアンは「エルミート演算子」であり、それに虚数単位をかけて指数の冪にしたei/Ht(以下これを時間発展演算子UH(t)と呼びます)は「ユニタリ演算子」です(このあたりの線形代数の用語にあまり馴染みがなくても、そういうものかと思ってもらえれば結構です)。

ユニタリ演算子は量子計算の言葉で言えばゲートにあたります。したがって、ある量子系に関して、その初期状態を量子レジスタで表現でき、時間発展演算子を量子コンピュータの基本ゲートの組み合わせで実装できれば、その系のダイナミクス(=時間発展)シミュレーションを量子コンピュータで行うことができます。

例:核磁気の歳差運動#

シミュレーションの詳しい話をする前に、これまで量子力学と疎遠だった方のために、ハミルトニアンや時間発展とは具体的にどういうことか、簡単な例を使って説明します。

空間中に固定されたスピン12原子核一つを考えます。ある方向(Z方向とします)のスピン±12の状態をそれぞれ|,|で表します。量子力学に馴染みのない方のための説明例で大いに量子力学的な概念を使っていますが、何の話かわからなければ「2つの基底ケットで表現される、量子ビットのような物理系がある」と考えてください。量子ビットのような物理系なので、系の状態は一般に||の重ね合わせになります。

時刻t0で系が|ψ(t0)=|にあるとします。時刻t1での系の状態を求めることは

|ψ(t1)=α(t1)|+β(t1)|

α(t1)β(t1)を求めることに相当します。ここでα(t0)=1,β(t0)=0です。

この原子核にX方向の一定磁場をかけます。非ゼロのスピンを持つ粒子はスピンベクトルσと平行な磁気モーメントμを持ち、磁場BのもとでエネルギーBμを得ます。ハミルトニアンとは実は系のエネルギーを表す演算子なので、この一定磁場だけに注目した場合の系のハミルトニアンは、何かしらの定数ωとスピンベクトルのX成分σXを用いてH=ωσXと書けます。

量子力学ではσXは演算子であり、||に対して

σX|=|σX|=|

と作用します。時間発展演算子UH(t)

UH(t)=exp(iωtσX)=n=01n!(iωt)n(σX)n=I+(iωt)σX+12(iωt)2(σX)2+16(iωt)3(σX)3

ですが(Iは恒等演算子)、上のσXの定義からわかるように(σX)2=Iなので

(10)#exp(iωtσX)=[1+12(iωt)2+]I+[(iωt)+16(iωt)3+]σX=cos(ωt)Iisin(ωt)σX

と書けます。したがって、

(11)#|ψ(t1)=UH(t1t0)|ψ(t0)=exp[iω(t1t0)σX]|=cos[ω(t1t0)]|isin[ω(t1t0)]|

です。任意の時刻t1のスピンの状態が基底||の重ね合わせとして表現されました。

このように、系のエネルギーの表式からハミルトニアンが決まり、その指数関数を初期状態に作用させることで時間発展後の系の状態が求まります。

ちなみに、|ψ(t1)t1=t0|t1=t0+π/(2ω)(i)|となり、以降||を周期的に繰り返します。実は、その間の状態はスピンがY-Z平面内を向いている状態に相当します。スピンが0でない原子核に磁場をかけると、スピンと磁場の方向が揃っていなければ磁場の方向を軸にスピンが歳差運動(すりこぎ運動)をします。これはコマが重力中で起こす運動と同じで、核磁気共鳴(NMR、さらに医学応用のMRI)の原理に深く関わります。

量子コンピュータ上での表現#

すでに触れましたが、上の例で核のスピンは量子ビットのように2つの基底ケットを持ちます(2次元量子系です)。さらに、お気づきの方も多いと思いますが、σX||への作用はXゲートの|0|1への作用そのものです。このことから、核磁気の歳差運動が極めて自然に量子コンピュータでシミュレートできることがわかるかと思います。

実際には、時間発展演算子はσXそのものではなくその指数関数なので、量子コンピュータでもexp(iθ2X)に対応するRx(θ)ゲートを利用します。これまで紹介されませんでしたが、Rxゲートはパラメータθをとり、

Rx(θ)|0=cosθ2|0isinθ2|1Rx(θ)|1=isinθ2|0+cosθ2|1

という変換を行います。上の核スピン系を量子コンピュータでシミュレートするには、1量子ビットでRx(2ω(t1t0))|0を計算する以下の回路を書けばいいだけです。

_images/19325ad9e81456e57817cf02ccd8bc3388640be6a0335c04d3f39af463eab509.png

ハミルトニアンの対角化#

再び量子コンピュータを離れて、量子・古典に関わらずデジタル計算機で量子ダイナミクスのシミュレーションをする際の一般論をします。

上の核スピンの例ではハミルトニアンが単純だったので、式(11)のように厳密解が求まりました。特に、導出において(σX)2=Iという恒等式が非常に重要でした。しかし、一般のハミルトニアンでは、何乗しても恒等演算子の定数倍にたどり着く保証がありません。

累乗して恒等演算子にならないようなハミルトニアンであっても、系の次元が小さい場合は「対角化」という作業で厳密解を得られます。ハミルトニアンの対角化とは、ハミルトニアンの作用が実数をかけることと等しくなるようなケットを探してくること、つまり

H|ϕj=ωj|ϕj,ωjR

が成り立つような|ϕjを見つけることを指します。このような|ϕjを「固有値ωjを持つHの固有ベクトル」と呼びます。「エネルギー固有状態」と呼ぶこともあります。系の次元がNであれば、独立な固有ベクトルがN個存在します。

例えば上の例ではH=ωσXですが、

(12)#|:=12(|+|)|:=12(||)

という2つの状態を考えると

σX|=|σX|=|

なので、これらが固有値±ωHの固有ベクトルとなっていることがわかります。

固有値ωjのハミルトニアンHの固有ベクトル|ϕjは自動的に時間発展演算子UH(t)の固有値eiωjtの固有ベクトルでもあります。

UH(t)|ϕj=exp(iHt)|ϕj=exp(iωjt)|ϕj.

したがって、系の初期状態|ψ(t0)

|ψ(t0)=j=0Ncj|ϕj

であれば、時刻t1での状態は

|ψ(t1)=j=0NcjUH(t1t0)|ϕj=j=0Neiωj(t1t0)cj|ϕj,

つまり、各固有ベクトルの振幅に、対応する位相因子をかけるだけで求まります。

再び核スピンの例を見ると、初期状態|ψ(t0)=|=1/2(|+|)なので、

|ψ(t1)=12(eiω(t1t0)|+eiω(t1t0)|)=12[(eiω(t1t0)+eiω(t1t0))|+(eiω(t1t0)eiω(t1t0))|]=cos[ω(t1t0)]|isin[ω(t1t0)]|

となり、式(11)が再導出できます。

このように、ハミルトニアンの対角化さえできれば、量子ダイナミクスのシミュレーションは位相をかけて足し算をするだけの問題に帰着します。しかし、上で言及したように、計算量の問題から、ハミルトニアンが対角化できるのは主に系の次元が小さいときに限ります。「対角化」という言葉が示唆するように、この操作は行列演算(対角化)を伴い、その際の行列の大きさはN×Nです。上の核スピンの例ではN=2でしたが、もっと実用的なシミュレーションの場合、系の量子力学的次元は一般的に関係する自由度の数(粒子数など)の指数関数的に増加します。比較的小規模な系でもすぐに対角化にスーパーコンピュータが必要なスケールになってしまいます。

鈴木・トロッター分解#

ハミルトニアンが対角化できない場合、ダイナミクスシミュレーションをするには、結局式(11)のように初期状態に時間発展演算子を愚直にかけていくことになります。これは、式(10)のようにUH(t)を閉じた形式で厳密に書けるなら簡単な問題ですが、そうでない場合は数値的に近似していく必要があります。その場合の常套手段は、行いたい時間発展(t1t0)を短い時間

Δt=t1t0M,M1

に分割し、Δtだけの時間発展UH(Δt)を考えることです。もちろん、UH(t)が閉じた形式で書けないのなら当然UH(Δt)も書けないので、時間を分割しただけでは状況は変わりません。しかし、Δtが十分短いとき、UH(Δt)に対応する計算可能な近似演算子U~H;Δtを見つけることができる場合があり、このU~H;Δtでの状態の遷移の様子がわかるのであれば、それをM回繰り返すことで、求める終状態が近似できることになります。

例えば、通常Hはわかっており、任意の状態|ψに対してH|ψが計算できるので、O((Δt)2)を無視する近似で

U~H;Δt=IiΔtH

とすれば、まずH|ψ(t0)を計算し、それをiΔt/倍して|ψ(t0)から引き、その結果にまたHをかけて、…という具合に|ψ(t1)が近似計算できます[1]

しかし、このスキームは量子コンピュータでの実装に向いていません。上で述べたように量子コンピュータのゲートはユニタリ演算子に対応するのに対して、IiΔt/Hはユニタリでないからです。代わりに、量子コンピュータでのダイナミクスシミュレーションでよく用いられるのが鈴木・トロッター分解という近似法です[NC00b]

鈴木・トロッター分解が使えるケースとは、

  • UH(t)は量子回路として実装が難しい。

  • ハミルトニアンがH=k=1LHkのように複数の部分ハミルトニアン{Hk}kの和に分解できる。

  • 個々のHkに対してはUHk(t)=exp(itHk)が簡単に実装できる。

のような場合です。もしもHHkが演算子ではなく単なる実数であれば、exp(kAk)=keAkなので、UH(t)=kUHk(t)となります。ところが、一般に線形演算子A,Bに対して、特殊な条件が満たされる(ABが「可換」である)場合を除いて

exp(A+B)exp(A)exp(B)

なので、そのような簡単な関係は成り立ちません。しかし、

exp(iΔtH)=k=1Lexp(iΔtHk)+O((Δt)2)

という、Baker-Campbell-Hausdorfの公式の応用式は成り立ちます。これによると、時間分割の極限では、

limMΔt0[k=1Lexp(iΔtHk)]M=exp(iH(t1t0)).

つまり、UH(Δt)

U~H;Δt=kUHk(Δt)

で近似すると、[U~H;Δt]MUH(t1t0)の間の誤差はΔtを短くすることで[2]いくらでも小さくできます。

鈴木・トロッター分解とは、このように全体の時間発展UH(t1t0)を短い時間発展UH(Δt)の繰り返しにし、さらにUH(Δt)をゲート実装できる部分ユニタリの積kUHk(Δt)で近似する手法のことを言います。

なぜ量子コンピュータが量子ダイナミクスシミュレーションに向いているか#

鈴木・トロッター分解がダイナミクスシミュレーションに適用できるには、ハミルトニアンが都合よくゲートで実装できるHkに分解できる必要があります。これが常に成り立つかというと、答えはyes and noです。

まず、2n次元線形空間に作用するエルミート演算子は、n個の2次元部分系に独立に作用する基底演算子{I,σX,σY,σZ}の積の線形和に分解できます。σX以外のパウリ演算子σYσZはここまで登場しませんでしたが、重要なのは、2次元量子系に作用するσX,σY,σZがそれぞれ量子ビットに作用するX,Y,Zゲート[3]に、パウリ演算子の指数関数がそれぞれRx,Ry,Rzゲート(総じて回転ゲートと呼びます)に対応するということです。つまり、対象の物理系の量子レジスタへの対応付けさえできれば、そのハミルトニアンは必ず基本的なゲートの組み合わせで表現できます。

しかし、nビットレジスタに作用する基底演算子の組み合わせは4n通りあり、最も一般のハミルトニアンではその全ての組み合わせが寄与することも有りえます。その場合、指数関数的に多くのゲートを用いてしか時間発展演算子が実装できないことになります。それでは「都合よく分解できる」とは言えません。

そもそも量子コンピュータで量子ダイナミクスシミュレーションを行う利点は、その計算効率にあります。

シミュレートする量子系の次元を2nとしたとき、古典計算機では、仮にハミルトニアンが対角化できても2n回の位相因子の掛け算と同じ回数だけの足し算を行う必要があります。ハミルトニアンが対角化できず、時間をMステップに区切って近似解を求めるとなると、必要な計算回数はO(2nM)となります。

一方、同じ計算にnビットの量子コンピュータを使うと、対角化できない場合のステップ数Mは共通ですが、各ステップで必要な計算回数(=ゲート数)はハミルトニアンHの基底演算子への分解Hkの項数Lで決まります。個々のHkは一般にO(n)ゲート要するので、計算回数はO(nLM)です。したがって、LO(1)であったりO(poly(n))nの多項式)であったりすれば、量子コンピュータでの計算が古典のケースよりも指数関数的に早いということになります。

したがって、逆に、ハミルトニアンが4n通りの基底演算子に分解されてしまっては(L=4n)、量子コンピュータの利点が活かせません[4]

幸いなことに、通常我々がシミュレートしたいと思うような物理系では、LはせいぜいO(n2)で、O(n)ということもしばしばあります。2体相互作用のある量子多体系などが前者にあたり、さらに相互作用が隣接した物体間のみである場合、後者が当てはまります。

実習:ハイゼンベルグモデルの時間発展#

モデルのハミルトニアン#

ハミルトニアンの分解と言われてもピンと来ない方もいるかもしれませんし、ここからはダイナミクスシミュレーションの具体例をQiskitで実装してみましょう。

ハイゼンベルグモデルという、磁性体のトイモデルを考えます。空間中一列に固定された多数のスピンを持つ粒子(電子)の系で、隣接スピンの向きによってエネルギーが決まるような問題です。

例えば、nスピン系で簡単な形式のハミルトニアンは

(13)#H=Jj=0n2(σj+1XσjX+σj+1YσjY+σj+1ZσjZ)

です。ここで、σj[X,Y,Z]は第jスピンに作用するパウリ演算子です。

ただし、式(13)の和の記法には実は若干の省略があります。例えば第j項をより正確に書くと、

In1Ij+2σj+1XσjXIj1I0

です。ここでは線形演算子間の「テンソル積」を表しますが、聞き慣れない方は掛け算だと思っておいてください。重要なのは、式(13)の各項が、上で触れたようにn個の基底演算子の積になっているということです。さらに、この系では隣接スピン間の相互作用しか存在しないため、ハミルトニアンがn1個の項に分解できています。

この系では、隣接スピン間の向きが揃っている(内積が正)のときにエネルギーが低くなります[5]。少し考えるとわかりますが、すべてのスピンが完全に同じ方向を向いている状態が最もエネルギーの低いエネルギー固有状態です。そこで、最低エネルギー状態から少しだけずらして、スピンが一つだけ直角方向を向いている状態を始状態としたときのダイナミクスをシミュレートしてみましょう。

核スピンのケースと同様に、それぞれのスピンについて+Z方向を向いた状態|を量子ビットの状態|0に、-Z方向の状態||1に対応づけます。このとき、上で見たように、パウリ演算子σX,σY,σZX,Y,Zゲートとが対応します。また、J=ω/2とおきます。

時間発展演算子は

UH(t)=exp[iωt2j=0n2(σj+1XσjX+σj+1YσjY+σj+1ZσjZ)]

ですが、ハミルトニアンの各項が互いに可換でないので、シミュレーションでは鈴木・トロッター分解を用いて近似します。各時間ステップΔtでの近似時間発展は

U~H;Δt=j=0n2exp(iωΔt2σj+1XσjX)exp(iωΔt2σj+1YσjY)exp(iωΔt2σj+1ZσjZ)

です。

量子ゲートでの表現#

これを回転ゲートと制御ゲートで表します。まずexp(iωΔt2σj+1ZσjZ)について考えてみましょう。この演算子のj-(j+1)スピン系の4つの基底状態への作用は

|j+1|jeiωΔt/2|j+1|j|j+1|jeiωΔt/2|j+1|j|j+1|jeiωΔt/2|j+1|j|j+1|jeiωΔt/2|j+1|j

です。つまり、2つのスピンの「パリティ」(同一かどうか)に応じて、かかる位相の符号が違います。

パリティに関する演算をするにはCNOTを使います。例えば以下の回路

_images/54ee4501b60981a03d5174544163800c71dfb037353ea1e686dfdf813f7d28b2.png

によって、計算基底|00,|01,|10,|11はそれぞれ

|00eiωΔt/2|00|01eiωΔt/2|01|10eiωΔt/2|10|11eiωΔt/2|11

と変換するので(確認してください)、まさにexp(iωΔt2σj+1ZσjZ)の表現になっています。

残りの2つの演算子も同様にパリティに対する回転で表せますが、CNOTで表現できるのはZ方向のパリティだけなので、先にスピンを回転させる必要があります。exp(iωΔt2σj+1XσjX)による変換は

|j+1|jeiωΔt/2|j+1|j|j+1|jeiωΔt/2|j+1|j|j+1|jeiωΔt/2|j+1|j|j+1|jeiωΔt/2|j+1|j

で、式(12)から、次の回路が対応する変換を引き起こすことがわかります(これも確認してください)。

_images/d8e484a5e0fdb3b346f5e69cbdc34013de312d6f8f07cc587df062ac5802e511.png

最後に、exp(iωΔt2σj+1YσjY)に対応する回路は

_images/259e5d4ad74fccd2eb071c862915c9db1a59cc75f463c372c1e9d60c1255ef78.png

です[6]

回路実装#

やっと準備が整ったので、シミュレーションを実装しましょう。実機で走らせられるように、n=5, M=10, ωΔt=0.1とします。上で決めたように、ビット0以外が|、ビット0が|という初期状態から始めます。各Δtステップごとに回路のコピーをとり、それぞれのコピーで測定を行うことで、時間発展の様子を観察します。

# まずは全てインポート
import numpy as np
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_aer.primitives import SamplerV2 as AerSampler
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as RuntimeSampler
from qiskit_ibm_runtime.accounts import AccountNotFoundError
# このワークブック独自のモジュール
from qc_workbook.dynamics import plot_heisenberg_spins
from qc_workbook.utils import operational_backend
n_spins = 5
M = 10
omegadt = 0.1

circuits = []

circuit = QuantumCircuit(n_spins)

# 第0ビットを 1/√2 (|0> + |1>) にする
circuit.h(0)

# Δtでの時間発展をM回繰り返すループ
for istep in range(M):
    # ハミルトニアンのn-1個の項への分解に関するループ
    for jspin in range(n_spins - 1):
        # ZZ
        circuit.cx(jspin, jspin + 1)
        circuit.rz(-omegadt, jspin + 1)
        circuit.cx(jspin, jspin + 1)

        # XX
        circuit.h(jspin)
        circuit.h(jspin + 1)
        circuit.cx(jspin, jspin + 1)
        circuit.rz(-omegadt, jspin + 1)
        circuit.cx(jspin, jspin + 1)
        circuit.h(jspin)
        circuit.h(jspin + 1)

        # YY
        circuit.p(-np.pi / 2., jspin)
        circuit.p(-np.pi / 2., jspin + 1)
        circuit.h(jspin)
        circuit.h(jspin + 1)
        circuit.cx(jspin, jspin + 1)
        circuit.rz(-omegadt, jspin + 1)
        circuit.cx(jspin, jspin + 1)
        circuit.h(jspin)
        circuit.h(jspin + 1)
        circuit.p(np.pi / 2., jspin)
        circuit.p(np.pi / 2., jspin + 1)

    # この時点での回路のコピーをリストに保存
    # measure_all(inplace=False) はここまでの回路のコピーに測定を足したものを返す
    circuits.append(circuit.measure_all(inplace=False))

print(f'{len(circuits)} circuits created')
10 circuits created

量子回路シミュレーターで実行し、各ビットにおけるZ方向スピンの期待値をプロットしましょう。プロット用の関数は比較的長くなってしまいますが実習の本質とそこまで関係しないので、別ファイルに定義してあります。関数はジョブの実行結果、系のスピンの数、初期状態、ステップ間隔を引数にとります。

# 初期状態 |0> x |0> x |0> x |0> x 1/√2(|0>+|1>) は配列では [1/√2 1/√2 0 0 ...]
initial_state = np.zeros(2 ** n_spins, dtype=np.complex128)
initial_state[0:2] = np.sqrt(0.5)

shots = 100000

simulator = AerSimulator()
# 今回はシミュレータと実機両方のSamplerを使うので、シミュレータベースのものをAerSamplerと呼んでいる(上のimport文を参照)
sampler = AerSampler()

circuits_sim = transpile(circuits, backend=simulator)
sim_job = sampler.run(circuits_sim, shots=shots)
sim_results = sim_job.result()

sim_counts_list = [result.data.meas.get_counts() for result in sim_results]

plot_heisenberg_spins(sim_counts_list, n_spins, initial_state, omegadt, add_theory_curve=True)
_images/094d4b026594306d8f66cdf0850d8bfed308ab4251090939d7a27a3f88d973e1.png

ビット0でのスピンの不整合が徐々に他のビットに伝搬していく様子が観察できました。

また、上のように関数plot_heisenberg_spinsadd_theory_curve=Trueという引数を渡すと、ハミルトニアンを対角化して計算した厳密解のカーブも同時にプロットします。トロッター分解による解が、厳密解から少しずつずれていっている様子も観察できます。興味があればΔtを小さく(Mを大きく)して、ずれがどう変わるか確認してみてください。

実機でも同様の結果が得られるか確認してみましょう。

# 利用できるインスタンスが複数ある場合(Premium accessなど)はここで指定する
# instance = 'hub-x/group-y/project-z'
instance = None

try:
    service = QiskitRuntimeService(channel='ibm_quantum', instance=instance)
except AccountNotFoundError:
    service = QiskitRuntimeService(channel='ibm_quantum', token='__paste_your_token_here__', instance=instance)

backend = service.least_busy(filters=operational_backend())

print(f'Job will run on {backend.name}')
sampler = RuntimeSampler(backend)

circuits_ibmq = transpile(circuits, backend=backend)
job = sampler.run(circuits_ibmq, shots=2000)
results = job.result()

counts_list = [result.data.meas.get_counts() for result in results]

plot_heisenberg_spins(counts_list, n_spins, initial_state, omegadt)