Qiskit Runtime Primitives入門(2022年秋 Lab1)#

オープニング・ストーリー - プロローグ#

こちらにあるオープニング・ビデオをご覧ください: Fall Challenge Opening Story

**クリックするとビデオの字幕を読むことができます**

あなたは、地球初の超光速宇宙船の船長として、人類を宇宙探査の時代へと導いています。

その旅では、魅力的な異世界や、夢にも思わなかった宇宙現象など、素晴らしいものを発見します。

根っからの研究者として探究心旺盛なあなたは、旅の途中で出会ったものすべてを調査します。

ある日、あなたはブラックホールを発見しました。理論的な研究を超えて、本物のブラックホールを直接研究するチャンスに心躍るあなたとクルー。

計算上、ブラックホールから安全な距離と思われる場所に宇宙船を停泊させる。しかし、どうやら計算を間違えたようだ。

そして、ブラックホールの引力はあまりにも強い。

少しずつ、ゆっくりと、しかし着実に、ブラックホールの中心へと引き寄せられていく...。

第1章#

黄色信号が発信されました。騒ぎが起きます。あなたとクルーはブラックホールの引力から逃れるために無数の方法を試しながら奔走します。しかし、その努力もむなしく、燃料を大量に消費してしまいます。

技術的にはまだ安全であり、ブラックホールの周りを緩やかな軌道で漂っているだけですが、非常にゆっくりと近づいていっています。あなたの計算によると、完全に落下するか、または重力の影響で宇宙船がバラバラになるかまでには数日かかる見込みです。

しかし、どうやって脱出すればよいだろうか?

いろいろ考えた末に、あなたがまだ試していない方法をチーフ・サイエンス・オフィサーが思いつきました:重力アシスト操縦です。ブラックホールを周回する惑星のひとつをスイングバイして、ブラックホールの影響から逃れるというものです。

うまくいくかもしれない。

その作戦に適した惑星を探す司令を出す直前に、通信機がスクランブルされたメッセージを受信しました。不思議なことに、そのメッセージは地球で使われているものと同じです。そんなはずはない!あなたたちは地球から初めて宇宙に来たのですから。

そして、数百キロメートル離れたところにあるビーコンの発信源を突き止めました。

すると、なんとそれは、あなたの宇宙船が搭載しているビーコンと同じ型のものだったのです。そんなことがあり得るのか?

それは、まるで時の流れに揉まれたように、あなたの船のものよりボロボロに磨り減っているように見えます。あなたは船内にあるすべてのビーコンをチェックし、それらがすべて無傷であることを発見しました。

あなたはこのビーコンとそのメッセージが重要であると感じ、このメッセージの解読に全力を注ぎます。

解読のためのプロトコルは、メッセージ・ヘッダーのスクランブル解除と、そのシーケンスを通信デコーダに入力することです。スクランブル解除ルーチンを構築し、デコード・シーケンス構築ルーチンを起動するために、以下のExerciseを完了しましょう!

Part I: Primitives入門#

Primitivesは、量子計算を行うユーザー、量子アルゴリズムを実装する開発者、複雑な問題を解決し新しいアプリケーションを提供する研究者のための、基礎的、基本的な構成要素として機能することを意図しています。

しかし、量子計算の観点からその意味を考える前に、「Primitive(プリミティブ)」という言葉が実際に何を意味するのか、それが私たちに何を意味するのかを考えてみよう。

  • 生物学者に「プリミティブとは何か」と尋ねると、おそらく「共通の祖先から受け継いだ性質、特性、特徴」と答えるでしょう。

  • 数学者であれば、「有限体において、その体の乗法的要素群の生成元となる要素」と答えるかもしれません。

  • CADモデルの設計者なら、「複雑な幾何学的形状を構築するために使用できる、システム上で最も単純な形状」と説明するかもしれません。

これらから、分かることは何でしょうか?これらの定義には、1つの共通点があります。それは、プリミティブとは、より複雑な要素を構成する基本的な要素であると定義していることです。

さて、一般的なコンピューティングに関して、「プリミティブ」という言葉を使うとどういう意味になるでしょうか。ここでは、言語プリミティブについて見ていきましょう。言語プリミティブとは、あるプログラミング言語で利用できる最も単純な不可分の要素と定義することができます。コンピューターの中のものはすべて0と1で記憶されているだけだと聞いたことがあると思いますが、全くその通りです。しかし、2進数をプログラミング言語の原始的な構成要素とすると、本当に大変なことになります。ここでは、高レベルのプログラミング言語に関連するプリミティブという言葉を定義して、プログラムを完全には分類できないですが、理解しやすいものに分類してみましょう。

その良い例がプリミティブ・データ型です。 選択した言語によっては、一般に分割できないデータ型があり、その使い方次第で、より複雑なデータ型を構築することができます。 例えば:一般にPythonでは、intfloatstringbooleanはプリミティブ・データ型であり、
また、string(文字列)の配列名前のリスト10進座標系のタプルinteger(整数)の集合は、プリミティブ・データ型によって構築されたプリミティブではないデータ型として考えられています。

では、プログラミングにおいて一般的な関数に関しても同じように定義できるのでしょうか?
プリミティブ関数は、使い方次第でより複雑で高度なプログラム要素やインタフェースを構築できる基本的なインタフェースやコードセグメントとして定義することができます。

なるほど、それが量子とどのような関係があるのですか?

言語プリミティブの定義で説明したように、技術的には2進数やマシンコードがプログラムをコンパイルする際の核となるプリミティブ構成要素であることはわかっていますが、マシンコードレベルではなく、よりアクセスしやすくプログラムを構築できるように分類した上位のプリミティブ用語を定義しているのです。 ほとんどの場合、主要なコンパイル言語には効率的なコンパイラー・ルーチンが組み込まれており、それを考えると、所有するシステムに最適化された実行ワークフローを期待しているので、プログラミング言語が定義する文法とプリミティブでコードを構築することができます。

では、量子についての質問です:量子計算のルーチンやワークフローに同様の構造を定義する方法はあるのでしょうか?

Qiskit Runtime Primitives入門:#

Qiskit Runtime architecture

クラウド上のQPUを中心とした所定の 量子ワークフロー を最大化するために、スケールでの効率的な実行でワークロードを最適化するために構築されたサービスに準拠したコンピューティング・プログラミングモデルを持つようになったのです。昨年から、Qiskitのランタイムサービスは、コンテナ型実行のコンセプトに基づいて構築されています。これは、複数の計算要素をパッケージ化して、あらゆるシステム上でポータブルに実行できるようにした実行モデルです。単体の回路をクラウドに送るのではなく、プログラム全体を依存関係とともにパッケージ化してクラウド上で実行することで、レイテンシーを節約し、反復ループのオーバーヘッドを低減します。

Qiskit Runtimeサービスの新しい開発により、Runtimeサービスだけでなく、Qiskit Runtimeサービスへのインターフェースとして機能する新しいプログラミングモデルを導入し、スケールの大きいプログラミング体験にフォーカスして更新されたものが Qiskit Primitives です。

SamplerとEstimator入門#

量子計算のためのPrimitivesを定義するために、まず、2つの候補を用意しました。量子計算の基本的な構成要素は複数存在するため、今後、さらに追加していく予定です。ここでは、2つの基本的なPrimitivesを定義しましょう。

量子コンピューターが古典コンピューターと異なる点は、出力に非古典的な確率分布を生成できることで、この点が重要です。そのため、同じ回路を複数回実行することで、確率分布の形で有用な情報を得ることができます。確率分布は、そこからサンプリングしたり、量を推定したりすることができるものです。

この2つの情報、a)確率分布のサンプリングb)量の推定、に基づき、私たちは2つのPrimitivesを命名しました:SamplerEstimatorです。

Sampler#

Samplerは、その名の通り、量子回路の出力からサンプリングして、擬似的な確率分布を推定します。出力からサンプリングすることで、量子回路の準確率分布全体を推定することができます。この機能は、回路設計の際に、回路全体の分布データを調べたり、作業する必要がある場合に、特に有効な機能です。つまり、ユーザーからの回路を入力とし、準確率のエラー軽減された読み出しを生成するプログラムです。このプログラムにより、ユーザーはエラー緩和を用いたショット結果をより適切に評価することができ、破壊的干渉の文脈で複数の関連するデータポイントの可能性をより効率的に評価することができるようになります。

これは要するに、回路を実行させたときに得られるおなじみの「counts」の出力と非常によく似ていますが、Samplerは、エラー緩和ルーチンの結果として準確率分布の出力を得ることができます。

簡単に言えば、より広い範囲の情報データを自由に使えるようになるのです。準確率分布の表現から得られる情報は,真の確率分布の尤度を調べたり,サンプリングのオーバーヘッドと引き換えに不偏の期待値ポイントを計算したりするのに,より関連性があると思われます。これらの分布はある意味で真の確率論と同じように振る舞いますが、異なるのは、元の理論の制約がいくつか緩和されていることです。その1つが、「負の」確率を表す負のデータポイントが存在する可能性です(ただし、集合的に和が1になることはあります)。また、このデータから、使用状況に基づいて真の確率分布を推定することもできます。例:グローバー探索、QSVMルーチン、スタビライザー計算、最適化ルーチンなど。

Smaplerは出力全体に対する完全な分布を与えますが、特定の結果に興味がある場合もあるでしょう。そこで、Estimator を見てみましょう!

Estimator#

Estimatorは、基本的に注目する演算子の期待値を計算し、受け取るものです。回路と観測値を取り込み、回路と観測値間を選択的にグルーピングして実行し、与えられたパラメータ入力に対する期待値と分散を効率的に評価するプログラム・インターフェースです。このPrimitiveにより、多くのアルゴリズムで必要とされる量子演算子の期待値の計算と解釈を効率的に行うことができます。

ある問題に対して最終的な解を求めることに興味があり、カウントの完全な分布を調べる必要がない人は、Estimator primitiveの方が便利だと感じるでしょう。このルーチンは、基本的にほとんどの近い将来の量子アルゴリズムに役立つもので、最も一般的な例は変分クラスのアルゴリズムです。Estimatorは回路だけでなく、量子観測量の期待値を計算するため、回路と観測量の入力が必要です。このような観測量には、分子の電子構造、最適化問題のコスト関数など、実に様々なものが含まれています。

Qiskit Runtimeを使うべき理由#

では、なぜこの新しいプログラミングパラダイムにこだわるのでしょうか?答えは Qiskit Runtimeサービスとのインターフェースと、その上に構築された強力なサービスやフレームワークを活用するためです。

前の章では、最適化されたワークフローを実現するためにコンパイラーに依存しながら、より高度な開発を可能にする言語プリミティブを定義しましたが、Runtimeは以下のような一般的に効果を期待される分野での効果が期待されます:

  • 効率 : バックエンド用に設計された反復処理ワークロードのための高度に最適化されたルーチンとオプションによって効率化されます。

  • レイテンシー : Sessionsフレームワークを使用したスケジューリング、ジョブの優先順位付け、共有キャッシュにより、投入されたルーチンのレイテンシーを低減し、結果を迅速に提供します。

  • 一貫性 : 既存のプリミティブモデルを補完する新しい機能を追加し、サービス全体の複雑なルーチンの上に構築する一貫したプログラミングモデルです。

  • カスタマイズ性 : コンテキストとジョブのパラメーターに基づいて回路をカスタマイズし、反復ルーチンをマネージします。

  • エラーの軽減と抑制 : 情報の質を高めるために、シンプルな抽象化されたインターフェースでエラー軽減・抑制のための最先端の研究成果を取り入れます。

最後に、低レイテンシーである(待ち時間が少ない)ことです。使われる環境はすでにすべてのQiskitにおける必要な要素を持ち、量子ハードウェアの近くで動作するため、古典ハードウェアと量子ハードウェア間のフィードバックループを必要とするアルゴリズムでは、大幅なスピードアップが期待できるのです。

Qiskit Runtimeサービスでは、Primitiveプログラミングモデルを使って、これらの利点をすぐに活用することができます。さっそく試してみましょう!このノートブックでは、SessionsフレームワークでSamplerとEstimator primitivesを使用する基本的な方法と、現在利用可能なエラー緩和の手法を紹介します。この後のノートブックでは、機械学習、最適化、化学の分野で、このprimitive プログラミングモデルを使って、より一般的なアルゴリズムルーチンを構築する方法を紹介していきます。

では、Qiskit Runtimeサービスを使ってこの新しいコンピューティングパラダイムでプログラムを構築する方法を確認しましょう。

#####  ライブラリーの導入
import time
import numpy as np
from qiskit import *
from qiskit.circuit import Parameter
from qiskit.quantum_info import Statevector, Pauli, SparsePauliOp
from qiskit.circuit.library import RealAmplitudes
from qiskit_aer import AerSimulator
import matplotlib.pyplot as plt
import matplotlib.ticker as tck
from qiskit.visualization import plot_histogram

Part II: Qiskit Runtimeを使い始める#

Qiskit Runtimeを使い始めることにします。

ローカルシステムで実行している場合、Qiskit Runtime パッケージをインストールする必要があるかもしれません:pip install qiskit-ibm-runtime でインストールできます。

ここではまずQiskitRuntimeServiceを定義してQiskit Runtimeのすべての要素を使用できるようにし、SamplerEstimator Primitive を必要に応じて呼び出すことができるようにします。

from qiskit_ibm_runtime import QiskitRuntimeService

以下のブロックは、現在の環境に対する Runtime アカウントの認証情報を保存します。channel キーワードの引数で、通常の IBM Quantum (channel='ibm_quantum') と IBM Cloud (channel='ibm_cloud')のどちらを使用しているのかを Runtime に知らせます。今回は、channelにibm_quantumを使用します。ここでは、すべての実行に ibm_quantum チャンネルを使用することにします。このセルは一度だけ実行することができ、認証情報はその環境用に保存されているはずです。

# Load your API token in .env
import os
from dotenv import load_dotenv

os.environ.pop('QXInstance', None)
os.environ.pop('QXToken', None)

load_dotenv()

instance = os.environ['QXInstance']
token = os.environ['QXToken']
service = QiskitRuntimeService(
    channel='ibm_quantum',
    instance=instance,
    token=token
)

これでサービスのセットアップが完了し、サービスは自動的にアクセス可能なすべてのバックエンドにアクセスできるようになりました。以下のコマンドを実行して、利用可能なバックエンドをすべて表示してみましょう。

service.backends()
[<IBMBackend('ibm_kyoto')>,
 <IBMBackend('ibm_osaka')>,
 <IBMBackend('ibm_sherbrooke')>,
 <IBMBackend('ibm_brisbane')>]

Part III: Primitivesの使い方#

それでは、Primitives構築の勘どころを、少しずつ押さえていきましょう。
まず、最初のPrimitivesはSampler primitiveです。ベルンシュタイン・ヴァジラニ アルゴリズムを使ってデモを行います。

Samplerを使った例#

Exercise1: ベルンシュタイン・ヴァジラニ アルゴリズム#

このアルゴリズムは、量子コンピューターを複雑な問題に適用した場合に優位性があることを示した特殊な量子アルゴリズムのうちの一つです。

入力と隠れたビット文字列\(s\)のビット積を返す関数があり,その長さは\(n\)です。

\[ f(x) = s ⋅ x (\mathrm{mod} 2) \]

隠れたビット文字列を見つけるには、関数 \(f\)\(n\) 回呼び出す必要があります。しかし、量子コンピューターを使用すると、関数を 1 回呼び出すだけ で100%の信頼度でこの問題を解くことができます。隠れたビット文字列を見つけるための量子ベルンシュタイン・ヴァジラニアルゴリズムは非常にシンプルです。

  1. 入力量子ビットを状態 \(|0\rangle^{\otimes n}\) に初期化し、出力量子ビットを \(|-\rangle\) に初期化する。

  2. アダマールゲートを入力レジスターに適用する。

  3. オラクルに問い合わせる。

  4. アダマールゲートを入力レジスターに適用する。

  5. 測定する。

注釈

ベルンシュタイン・ヴァジラニ アルゴリズムについて詳しくは、Qiskit YoutubeにあるCoding with Qiskit Season 1のエピソード6が参考になります。

以下に、ベルンシュタイン・ヴァジラニ アルゴリズムが隠れたビット文字列 "001" を見つける例を示します。

hidden = "001"
print(len(hidden))
3

この隠れたビット文字列には、3つの入力量子ビットと1つの出力量子ビットが必要です。

# Make a quantum circuit
qc = QuantumCircuit(4, 3)
display(qc.draw(output="mpl"))
_images/5f8491c792ff95b2da11edc0d5fb17347c2ac493a71310bf4cee3317e938ac32.png

ステップ 1. 入力量子ビットを状態 \(|0\rangle^{\otimes n}\) に初期化し、出力量子ビットを \(|-\rangle\) に初期化します。

最初に、すべての量子ビットは \(|0\rangle\) として初期化されるため、入力量子ビットにゲートを適用する必要はありません。 ただし、出力量子ビットの状態は \(|-\rangle\) として変更する必要があります。 そのために、X-gate を適用してから H-gate (アダマールゲート) を適用します。

qc.x(3)
qc.h(3)
display(qc.draw(output="mpl"))
_images/7cf95409f925b574bb570ea31cab366b68e7f5d6cc17c73195dbe3d71be45258.png

ステップ 2. アダマールゲートを入力レジスターに適用します。

各入力量子ビットにアダマールゲートを適用します。

qc.h(0)
qc.h(1)
qc.h(2)
display(qc.draw(output="mpl"))
_images/6e677cfabe80ee95ce8ebb71d0a7c742b636dc3917d65bd0cda706ba2c069d3f.png

ステップ 3. オラクルに問い合わせます。

CNOTゲートを使用してオラクルにクエリーを実行します。隠し回路が"001"であるため、CNOTゲートを量子ビット0と出力ゲートに適用します。

注意

Qiskitでは、ビットの番号が 右から左へ 割り当てられます。

qc.cx(0,3)
display(qc.draw(output="mpl"))
_images/10f00a50ddd2fdbfbc2295f0506acfa468912819a725a56dab0522c3227df59a.png

ステップ 4. アダマールゲートを入力レジスターに適用します。

各入力量子ビットにアダマールゲートを再度適用します。

qc.h(0)
qc.h(1)
qc.h(2)
display(qc.draw(output="mpl"))
_images/8d4b48924555392f524e573334102bd9840fc55041c713a5b90ab43b4e096098.png

ステップ 5. 測定します。

measure メソッドですべての入力量子ビットを測定します。

qc.measure(range(3), range(3))
display(qc.draw(output="mpl"))
_images/20177780b617f0d3ee01388da1da458ffde208c745253a83dac69bba8a733659.png

回路はシミュレーターで実行できます。下のセルを実行すると、結果を確認できます。

# Use local simulator
aer_sim = AerSimulator()
results = aer_sim.run(qc).result()
answer = results.get_counts()

print(answer)
{'001': 1024}

結果には、最初に設定した隠れたビット文字列"001"が表示されています。

Exercise

ベルンシュタイン・ヴァジラニ関数を作ってみましょう。パラメーターは1つだけで、これが隠しビット文字列です。この隠しビット文字列は "0 "と "1 "から成り立っています。以下のコードブロックでは、全ての隠しビット文字列でうまく動作する一般的な関数を構築する必要があります。

def bernstein_vazirani(string):
    
    ##### Save the length of string
    string_length = len(string)
    
    ##### Make a quantum circuit
    qc = QuantumCircuit(string_length+1, string_length)
    
    ##### Initialize each input qubit to apply a Hadamard gate and output qubit to |->
    
    ###### build your code here #####
    
    ###### Apply an oracle for the given string
    ###### Note: In Qiskit, numbers are assigned to the bits in a string from right to left
    

    ###### build your code here #####

    
    ###### Apply Hadamard gates after querying the oracle
    

    ###### build your code here ######
    
    ###### Measurement
    qc.measure(range(string_length), range(string_length))
    
    return qc
Hide code cell content
def bernstein_vazirani(string):

    # Save the length of string
    string_length = len(string)

    # Make a quantum circuit
    qc = QuantumCircuit(string_length+1, string_length)

    # 1. Initialize each input qubit to apply a Hadamard gate and output qubit to |->
    qc.x(string_length)
    qc.h(range(string_length+1))
    qc.barrier()

    # 2. Apply an oracle for the given string
    # Note: In Qiskit, numbers are assigned to the bits in a string from right to left 
    string = string[::-1] # Reverse password to fit qiskit's qubit ordering
    for q in range(string_length):
        if string[q] == '1':
            qc.cx(q, string_length)
    qc.barrier()

    # 3. Apply Hadamard gates after querying the oracle
    qc.h(range(string_length))

    # Measurement
    qc.measure(range(string_length), range(string_length))

    return qc
  1. 最初に初期化を行います。入力量子ビット数はstring_length、出力量子ビット数は1になります。初期化では、qc.h(range(string_length+1))で入力量子ビットと出力量子ビットにアダマールゲート、qc.x(string_length)で出力量子ビットを\(\ket{-}\)にします。 rangeを用いることで複数量子ビットにゲートを適用できます。

  2. 次にオラクルを作成します。引数であるstringが1である場合(string[q] == '1')、CNOTゲートを適用します。Qiskitでは、ビットの番号が右から左へ割り当てられるため、関数の入力であるstringの順番をstring = string[::-1]で反転させます。

  3. 最後にqc.h(range(string_length))で入力量子ビットにアダマールゲートを適用します。

ここで、上記の関数を呼び出して、定義されているベルンシュタイン・ヴァジラニ回路を構築します。

qc1 = bernstein_vazirani('111')
display(qc1.draw(output="mpl"))
_images/3bf736d3bef606add8a8ecc7b7a1a4e74850888bb4385ba695ce7004c70219ae.png

この回路を実行する前に、先ほど述べたように、1つのSamplerセッションに対して複数の回路呼び出しと実行が可能です。そのことを示すために、もう1つベルンシュタイン・ヴァジラニ回路を作ることにします。

qc2 = bernstein_vazirani('000')
display(qc2.draw(output="mpl"))
_images/c3911e61e407c0df03193b0a047bd1278961b6923800b74b005259915fb1336f.png

回路実行のためにRuntimeを使う#

Qiskit Runtimeで量子回路を実行する前に、3つのステップがあることを忘れないでください。

  1. 使用するBackendを設定する

  2. Sessionを設定する

  3. sessionの中でPrimitivesの Sampler または Estimator をインスタンス化

まず、使用するバックエンドを設定しましょう。 ここでは、クラウド上のibmq_qasm_simulator上でルーチンを実行することにします。ローカル上のAerSimulator上でルーチンを実行することにします。

重要

Quantum Utilityへの移行に伴い、当時Challengeで利用していたIBM Quantum® クラウドシミュレータは廃止となりました。
シミュレータ上での実行はの下記のいずれかを利用することになります。(詳細はこちら)

  • qiskit_ibm_runtime.fake_providerのFake backends → 特定のハードウェアを前提にシミュレーションしたい場合

  • qiskit_aerのAerSimulator → より精度の高いシミュレーション結果を知りたい場合

Fake backendsを利用は2024年 Lab4で紹介されているので、ここではAerSimulatorを利用していきます。

backend = AerSimulator()

注意

Primitivesの Ver.2のリリースに伴い、当時のChallengeと環境が大きく変更されています。 この中では当時の説明・コードを参照しつつ、V2の変更点を見ていきましょう。

最初にSessionの概念について少し触れておきます。
クラウド上でPrimitivesを使用するには、セッションを安全にオープン/クローズするために、コンテキスト・マネージャーを使用する必要があります。これは、コンストラクターがパラメーターを取り込み、SamplerとEstimatorのメソッドがキュー内のジョブを渡すSessionキーワードを使用することで実現されます。

Options は、現在のsessionとそのパラメーターを設定し、その実行環境を制御するために使用します。このフォーマットでは、複数の様々なprimitiveなプログラムを1つのパッケージとして実行することができます。これにより、1つのsessionで定義されたSamperやEstimatorの呼び出しを1つのインスタンスで渡すことが容易になります。
また、ジョブパイプラインでsessionルーチン全体をブロックにし、sessionコンストラクターで定義されたprimitiveコールをジョブ・キューで順次実行させることができます。

SamplerV2EstimatorV2ではそれぞれ別のオプションクラスが存在するため、OptionsクラスはPrimitives V2では利用しません。またオプションの設定方法も多様化しました。詳細はこちら

セッションをコンストラクターを作成してみましょう。
with文を使ってSessionを作り、その中にベルンシュタイン・ヴァジラニ回路を実行するためのSamplerインスタンスを作成します。ここでのwith文はコンテキスト・マネージャーを意味し、コンストラクターの初期パラメーターとして定義したサービスを渡すために使用します。

後述しますが、ローカルテストではSession構文はサポートしなくなりました。

Samplerでジョブを投入するには、run メソッドでパラメータを指定します。このメソッドには circuits を渡す必要があり、1つのcircuitか QuantumCircuit のリストを渡すことができます。 このコンストラクターの最大の特徴は、Sampler の runコールに渡されるリストを変更するだけで、渡すcircuit数を素早く拡張できることです。

from qiskit_ibm_runtime import Options
options = Options(simulator={"seed_simulator": 42}) # Do not change values in simulator
with Session(service=service, backend=backend):
    sampler = Sampler(options=options)
    job = sampler.run(circuits=[qc1,qc2])

実行結果 IBMInputValueError: 'The instruction h on qubits (0,) is not supported by the target system.  Circuits that do not match the target hardware definition are no longer supported after March 4, 2024.  See the transpilation documentation (https://docs.quantum.ibm.com/transpile) for instructions to transform circuits and the primitive examples (https://docs.quantum.ibm.com/run/primitives-examples) to see this coupled with operator transformations.'

Samplerを実行すると、RuntimeJob オブジェクトが返されます。このオブジェクトから result メソッドを使って結果を抽出し、返されたデータを確認します。そして、この返された結果から、興味のあるデータを個別に選択することができます。詳細はこちらです: RuntimeJob

SamkplerV2ではRuntimeJobV2 オブジェクトが返されます。resultメソッドを使って結果を抽出する点は変わりませんが、相違点は多いのでRuntimeJobV2のドキュメントを確認してください。例えばV1では結果を擬確率分布の形式で返していますが、V2では各ショットの測定結果をビット文字列の形式で返します。

result.quasi_dists # => [{7: 1.0}, {0: 1.0}]

Primitive V2の変更ポイント

変更ポイント1

SamplerV2, EstimatorV2のリリースに伴い、V1の利用は非推奨となりました。

The Sampler and Estimator V1 primitives have been deprecated as of qiskit-ibm-runtime 0.23.0 and will be removed no sooner than 3 months after the release date. Please use the V2 Primitives.

変更ポイント2

量子回路の実行にあたり、回路とオブザーバブルは実行先のバックエンドがサポートする操作に最適化(ISA: Instruction Set Architecture)しなければならなくなりました。このLabではtranspile対応のみ記述しますが、詳細はChallenge2024 Lab2で解説されているのでこちらをご覧ください。

変更ポイント3

Sessionの構文は、シミュレータやローカルテスト時にはサポートしなくなり(文法エラーが出るわけではなく、無視される)、記述が不要となりました。

UserWarning: Session is not supported in local testing mode or when using a simulator.

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

pm = generate_preset_pass_manager(target=backend.target, optimization_level=1)
isa_qc1 = pm.run(qc1)
isa_qc2 = pm.run(qc2)
from qiskit_ibm_runtime import SamplerV2 as Sampler, EstimatorV2 as Estimator

sampler = Sampler(backend=backend)
pub_results = sampler.run([isa_qc1, isa_qc2]).result()
[pub_result.data.c.get_int_counts() for pub_result in pub_results]
[{7: 1024}, {0: 1024}]

この結果、各回路から2つの結果が得られます。最初の回路qc1の隠れ文字列は111であり、最初の結果は7(7は2進数で111)です。この確率は1.0であり、この回路は100%確実に7を返すということです。同様に、2番目の結果は0(0は2進数で000)であり、その確率も1.0です。

このように、回路を作るときに渡したビット列と同じ結果になります。どうしてこんなことが可能なのか、不思議に思われるかもしれません。下の画像を見てください:

4つのHゲートの間にCNOTゲートがあり、これは反転したCNOTゲートと同じす。これはキックバック(または位相キックバック)の例で、ゲートによってある量子ビットに加えられた固有値が、制御演算によって別の量子ビットにキックバックされるのです。これにより、ベルンシュタイン・ヴァジラニ回路は、隠されたビット列を明らかにすることができます。

Exercise2: パラメーター化された回路#

Primitivesのメリットの1つは、パラメーター化された回路に複数のパラメーターをバインドするのが簡単になることです。回路にパラメーターをバインドする方法の例については、 Qiskit Document をご覧ください。

ここでは制御Pゲート(制御位相ゲート)を使ったキックバックの別の例を紹介しましょう。Pゲート を回転パラメーターthetaでパラメター化します。

theta = Parameter('theta')

qc = QuantumCircuit(2,1)
qc.x(1)
qc.h(0)
qc.cp(theta,0,1)
qc.h(0)
qc.measure(0,0)

qc.draw("mpl")
_images/c7b795934e2b50076d930e81597eec0c97db1e72c226f42fc0b7cd50e6690407.png

上記の回路はパラメーター化されており、固有値は測定のために量子ビット 0 に戻されます。 キックバックの度合いは、パラメーター theta によって決まります。 下のセルで、上記の回路のパラメーターをリストとして定義します。ここでのパラメーターは、\(0\) から \(2\pi\) までを50等間隔のポイントで分割したものになります。

位相制御ゲートについて、異なる位相で回路を評価してみましょう:

phases = np.linspace(0, 2*np.pi, 50) # Specify the range of parameters to look within 0 to 2pi with 50 different phases

# Phases need to be expressed as list of lists in order to work
individual_phases = [[ph] for ph in phases]

上記の回路に適用する前に、ブロッホ球を使ってどのように見えるかをイメージしてみましょう。

# help understanding of how its phase is moving
from qiskit.visualization import plot_bloch_multivector

states = []

for i in range(0, 50, 10):
    
    temp = QuantumCircuit(2,1)
    temp.x(1)
    temp.h(0)
    temp.cp(individual_phases[i][0],0,1)
    temp.h(0)
    
    state = Statevector(temp)
    states.append(state)
plot_bloch_multivector(states[0])
_images/476dcb061e8b3f38d6329b342870e81fd539dcd226574ed0a4c11d15c26d5282.png
plot_bloch_multivector(states[1])
_images/7647213027905442a9349494e828893f1a922cb183c0cb2341c163f798c488a8.png
plot_bloch_multivector(states[2])
_images/6abcf19080e5c8f06976a37f49d29383199657fef325ebc524ddd57224752316.png
plot_bloch_multivector(states[3])
_images/b18816ac9226ca4744c00d47e2f2cf523d8411a2a3ac841199bbb0951805deb2.png
plot_bloch_multivector(states[4])
_images/f661377ee4d0ddcba91e60b5b16edab5657e1fb8703868cc5a4164dce69f1ca6.png

それぞれの位相で状態が変化し、Y-Z軸に沿って回転しているのがわかると思います。さて、このパラメーターリストを回路 qc に適用して、次の演習を行ってみましょう。

Exercise

パラメーターをパラメーター化された回路リストにバインドし、個々の位相individual_phasesSamplerV2を使用して上で作成したqc回路にバインドします。

ここでは、再び SamplerV2 を使って、これらの回路をすべて実行し、パラメーターをバインドします。

SamplerV2の run メソッドには、以下のパラメーターがあります。

pubs: Iterable[SamplerPubLike] - PUB(Primitive Unified Blocs: 量子回路、オブザーバブル、パラメータ群、オプション) ライクなオブジェクトのイテラブル (ここでは量子回路とパラメータ群のリストと考えてください。)
shots: int - sampler pubごとにサンプリングするショットの総数

注意

Samplerの run メソッドには、以下のパラメーターがあります。

circuits - 1つ以上の回路オブジェクト。
parameter_values - 回路にバインドされるパラメーター。

このExerciseにpassするために、以下のコードセルを完成させてください。

with Session(service=service, backend=backend):    
    sampler = # build your code here
    job = # build your code here
    result = # build your code here
pm = generate_preset_pass_manager(target=backend.target, optimization_level=1)
isa_qc = pm.run(qc)
Hide code cell content
sampler = Sampler(backend=backend)
pubs = [(isa_qc, parameter_values) for parameter_values in individual_phases]
pubs_result = sampler.run(pubs).result()

上のコードセルは、パラメーター化された回路を受け取り、Runtimeサービスを使ってバックエンドで実行します。このルーチンは各パラメーターを定義された回路に結びつけ、その結果得られたすべての回路を実行し、集合的な結果を得ることができます。

それでは、得られた結果と理論的に推測される結果をプロットしてみましょう。これらの回路について、1つの状態となる確率の準分布を求めます。各回路は位相パラメーターとして異なるtheta値を持つことになります。

# The probablity of being in the 1 state for each of these values
num_shots = pubs_result[0].data.c.num_shots
pubs_result_counts_list = [pub_result.data.c.get_int_counts() for pub_result in pubs_result]
prob_values = [dist.get(1, 0) / num_shots for dist in pubs_result_counts_list]

plt.plot(phases, prob_values, 'o', label='simulator')
plt.plot(phases, np.sin(phases/2,)**2, label='theory')
plt.xlabel('Phase')
plt.ylabel('Probability')
plt.legend()
<matplotlib.legend.Legend at 0x7f4c3330f7d0>
_images/8247f1b293bc62f442e409f74aca74ad92fa291508b641911fa415afb0263b2a.png

黄色い線が理論値で、青い点はバックエンドで実行したときの値です。ほぼ理論と一致していますが、シミュレータ固有のランダム性により、結果の分布のカーブには若干のずれが生じています。

ここまでは擬確率分布を見てきましたが、期待値の評価という観点からも見てみましょう。

Estimatorの例#

Estimatorは、量子演算子の期待値を計算し、受け取ったものを提供します。Estimatorは、「測定のない」回路である必要があります。なぜかというと、VQEのようなアルゴリズムを実行する場合、Estimatorはハミルトニアンを得るために単一量子ビットの回転をバインドするので、測定をすることができないからです。

display(qc.draw(output="mpl"))
_images/c7b795934e2b50076d930e81597eec0c97db1e72c226f42fc0b7cd50e6690407.png

現在の回路qcには測定があるので、remove_final_measurementsでこれを削除します。

qc_no_meas = qc.remove_final_measurements(inplace=False)
display(qc_no_meas.draw(output="mpl"))
_images/18334f0a82e830bb546219a174b1a0167bb904ade21cb5c6af80fbbf7afe871b.png

期待値を計算するために、回路にオブザーバブルを設定する必要があります。ここでは、'ZZ' を使います。オブザーバブルの長さは回路の量子ビットの数と同じであることに注意してください。

ZZ = SparsePauliOp.from_list([("ZZ", 1)])
isa_ZZ = ZZ.apply_layout(qc_no_meas.layout)

期待値は以下の式で算出されます。

\[ \langle ZZ\rangle =\langle \psi | ZZ | \psi\rangle=\langle \psi|(|0\rangle\langle 0| -|1\rangle\langle 1|)\otimes(|0\rangle\langle 0| - |1\rangle\langle 1|) |\psi\rangle =|\langle 00|\psi\rangle|^2 - |\langle 01 | \psi\rangle|^2 - |\langle 10 | \psi\rangle|^2 + |\langle 11|\psi\rangle|^2 \]

上の式をよく見てから、次のセルを実行することを強くお勧めします。

estimator = Estimator(backend=backend)
pubs = [(qc_no_meas, ZZ, parameter_values) for parameter_values in individual_phases]
job = estimator.run(pubs)
param_results = job.result()
exp_values = [float(param_result.data.evs) for param_result in param_results]

plt.plot(phases, exp_values, 'o', label='real')
plt.plot(phases, 2*np.sin(phases/2,)**2-1, label='theory')
plt.xlabel('Phase')
plt.ylabel('Expectation')
plt.legend()
<matplotlib.legend.Legend at 0x7f4c2b7efa50>
_images/e984b3d506eef25d0b37255c05986853a850837ee3cf67085716b83886b2cb81.png

ハミルトニアンについて#

Estimatorの面白い使い方の一つは、特にオブザーバブルに関するハミルトニアンを計算するのに使えることです。

ハミルトニアンは量子力学的な演算子であり、運動エネルギーと位置エネルギーを含む系内の全エネルギー情報を持っています。
そのためハミルトニアンの計算が必要であり、そのエネルギー値を計算できれば、自然界におけるエネルギーや、機械学習におけるコストを計算することができます。
基底状態や励起状態を見つけることができるので量子物理学、量子化学、量子機械学習と密接に関係しています。

ハミルトニアンを計算するためには、パラメーター化された回路が必要です。RealAmplitudes を使えば、ランダムなパラメーター化された回路を簡単に作ることができます。以下にコード例を示します。

ansatz = RealAmplitudes(3, reps=2)  # create the circuit on 3 qubits
ansatz.decompose().draw("mpl")
_images/85f73364f1ce27040cb6671f7b6b7133b40eb0ea517c08f8301df3399ca02355.png

このansatzは3量子ビットの回路で、reps は2です。この場合、パラメータの総数は\(3 \times (2+1) = 9\)となります。

Exercise 3: 期待値計算の推定ルーチンを構築#

Exercise

特定の観測値に関するカスタムハミルトニアンの期待値を計算する推定ルーチンを構築します。解答は、EstimatorResult である必要があります。

\( \langle \psi_1(\theta) \lvert H_1 \lvert \psi_1(\theta)\rangle\), \( \langle \psi_2(\theta) \lvert H_2 \lvert \psi_2(\theta)\rangle\), \( \langle \psi_3(\theta) \lvert H_3 \lvert \psi_3(\theta)\rangle\)をEstimatorを使って計算します。回路は全て5量子ビットで構成されています。

  1. RealAmplitudesを使ってランダムな回路を作ります; \(\psi_1(\theta) \)はreps = 2 、 \( \psi_2(\theta) \)はreps = 3、 \( \psi_3(\theta) \)はreps = 4です。

##### Make three random circuits using RealAmplitudes

psi1 = # build your code here
psi2 = # build your code here
psi3 = # build your code here
  1. SparsePauliOpを使ってハミルトニアンを作ります。

    • \( H_1 = X_1Z_2 + 3Y_0Y_4 \)

    • \( H_2 = 2X_3 \)

    • \( H_3 = 3Y_2 + 5Z_1X_3 \)

##### Make hamiltonians using SparsePauliOp

H1 = # build your code here
H2 = # build your code here
H3 = # build your code here
  1. numpy.linspaceを使って0から1までの theta の値を等間隔に並べたリストを作ります。なお、各回路のrepsが異なるため、パラメーターの数が異なります。

##### Make a list of evenly spaced values for theta between 0 and 1

theta1 = # build your code here
theta2 = # build your code here
theta3 = # build your code here
  1. セル内で定義された options を持つEstimatorを使って、各期待値を計算します。

##### Use the Estimator to calculate each expectation value

estimator = # build your code here

# calculate [ <psi1(theta1)|H1|psi1(theta1)>,
#             <psi2(theta2)|H2|psi2(theta2)>,
#             <psi3(theta3)|H3|psi3(theta3)> ]
# Note: Please keep the order
job = # build your code here    

pubs_result = # build your code here
Hide code cell content
psi1 = RealAmplitudes(num_qubits=5, reps=2)
psi2 = RealAmplitudes(num_qubits=5, reps=3)
psi3 = RealAmplitudes(num_qubits=5, reps=4)

pm = generate_preset_pass_manager(target=backend.target, optimization_level=1)
isa_psi1 = pm.run(psi1)
isa_psi2 = pm.run(psi2)
isa_psi3 = pm.run(psi3)
Hide code cell content
H1 = SparsePauliOp.from_list([("IIZXI", 1), ("YIIIY", 3)])
H2 = SparsePauliOp.from_list([("IXIII", 2)])
H3 = SparsePauliOp.from_list([("IIYII", 3), ("IXIZI", 5)])

isa_H1 = H1.apply_layout(isa_psi1.layout)
isa_H2 = H2.apply_layout(isa_psi2.layout)
isa_H3 = H3.apply_layout(isa_psi3.layout)

SparsePauliOpを使ってハミルトニアンを定義しています。H1の二つ目の項は以下のように示されます。

("YIIIY", 3)

左の"YIIIY"\(Y_{0}Y_{4}\)を示し、右の3は係数を示します。 この時、右から左の順番であることやIdentity gateが含まれていることに注意してください。

また、バックエンドに渡す量子回路、オブザーバブルはISAに従っている必要があること忘れないようにしましょう。

Hide code cell content
theta1 = np.linspace(0, 1, 15)
theta2 = np.linspace(0, 1, 20)
theta3 = np.linspace(0, 1, 25)

0から1までの \(\theta\) の値を各回路のパラメータ数に合わせて分割します。 例えば、

RealAmplitudes(num_qubits=5, reps=2, insert_barriers=True)

で構築された回路の場合、パラメータの数は15になります。

Hide code cell content
estimator = Estimator(backend=backend)

# calculate [ <psi1(theta1)|H1|psi1(theta1)>,
#             <psi2(theta2)|H2|psi2(theta2)>,
#             <psi3(theta3)|H3|psi3(theta3)> ]
# Note: Please keep the order
pubs = [
    (isa_psi1, isa_H1, theta1),
    (isa_psi2, isa_H2, theta2),
    (isa_psi3, isa_H3, theta3)
]
job = estimator.run(pubs)
pubs_result = job.result()

参考文献#

補足情報#

Created by: Dayeong Kang, Yuri Kobayashi, Vishal Bajpe, Kifumi Numata

Advisor: Ikko Hanamura

Creative assets by: Radha Pyari Sandhir

Translated and adapted by: Kifumi Numata, Yuri Kobayashi

Version: 1.0