【参考】足し算を実機で行う

【参考】足し算を実機で行う#

実習の内容の延長です。ここでは並列足し算回路を実機で実行します。

効率化前後の回路の比較#

実習のおさらいをすると、まずもともとの足し算のロジックをそのまま踏襲した回路を作り、それではゲート数が多すぎるので効率化した回路を作成しました。

実は効率化した回路でもまだゲートの数が多すぎて、4ビット+4ビットの計算では答えがスクランブルされてしまいます。回路が小規模になればそれだけ成功確率も上がりますので、\((n_1, n_2)\)の値として(4, 4)以外に(3, 3)、(2, 2)、(1, 1)も同時に試すことにしましょう。

# まずは全てインポート
import numpy as np
import matplotlib.pyplot as plt
from qiskit import QuantumRegister, QuantumCircuit, transpile
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit_ibm_runtime.accounts import AccountNotFoundError
from qc_workbook.optimized_additions import optimized_additions
from qc_workbook.utils import operational_backend, find_best_chain

print('notebook ready')
# 利用できるインスタンスが複数ある場合(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(min_num_qubits=13, filters=operational_backend())

print(f'Using backend {backend.name}')

実習と全く同じsetup_addition関数と、次のセルで効率化前の回路を返すmake_original_circuit関数を定義します。

def setup_addition(circuit, reg1, reg2, reg3):
    """Set up an addition subroutine to a circuit with three registers
    """

    # Equal superposition in register 3
    circuit.h(reg3)

    # Smallest unit of phi
    dphi = 2. * np.pi / (2 ** reg3.size)

    # Loop over reg1 and reg2
    for reg_ctrl in [reg1, reg2]:
        # Loop over qubits in the control register (reg1 or reg2)
        for ictrl, qctrl in enumerate(reg_ctrl):
            # Loop over qubits in the target register (reg3)
            for itarg, qtarg in enumerate(reg3):
                # C[P(phi)], phi = 2pi * 2^{ictrl} * 2^{itarg} / 2^{n3}
                circuit.cp(dphi * (2 ** (ictrl + itarg)), qctrl, qtarg)

    # Insert a barrier for better visualization
    circuit.barrier()

    # Inverse QFT
    for j in range(reg3.size // 2):
        circuit.swap(reg3[j], reg3[-1 - j])

    for itarg in range(reg3.size):
        for ictrl in range(itarg):
            power = ictrl - itarg - 1 + reg3.size
            circuit.cp(-dphi * (2 ** power), reg3[ictrl], reg3[itarg])

        circuit.h(reg3[itarg])

def make_original_circuit(n1, n2):
    """A function to define a circuit with the original implementation of additions given n1 and n2
    """
    n3 = np.ceil(np.log2((2 ** n1) + (2 ** n2) - 1)).astype(int)

    reg1 = QuantumRegister(n1, 'r1')
    reg2 = QuantumRegister(n2, 'r2')
    reg3 = QuantumRegister(n3, 'r3')

    # QuantumCircuit can be instantiated from multiple registers
    circuit = QuantumCircuit(reg1, reg2, reg3)

    # Set register 1 and 2 to equal superpositions
    circuit.h(reg1)
    circuit.h(reg2)

    setup_addition(circuit, reg1, reg2, reg3)

    circuit.measure_all()

    return circuit

(4, 4)から(1, 1)までそれぞれオリジナルと効率化した回路の二通りを作り、全てリストにまとめてバックエンドに送ります。

# count_ops()の結果をテキストにする関数
def display_nops(circuit):
    nops = circuit.count_ops()
    text = []
    for key in ['rz', 'x', 'sx', 'cx']:
        text.append(f'N({key})={nops.get(key, 0)}')

    return ', '.join(text)

# オリジナルと効率化した回路を作る関数
def make_circuits(n1, n2, backend):
    print(f'Original circuit with n1, n2 = {n1}, {n2}')
    circuit_orig = make_original_circuit(n1, n2)

    print('  Transpiling..')
    circuit_orig = transpile(circuit_orig, backend=backend, optimization_level=3)

    print(f'  Done. Ops: {display_nops(circuit_orig)}')
    circuit_orig.name = f'original_{n1}_{n2}'

    print(f'Optimized circuit with n1, n2 = {n1}, {n2}')
    circuit_opt = optimized_additions(n1, n2)

    n3 = np.ceil(np.log2((2 ** n1) + (2 ** n2) - 1)).astype(int)

    print('  Transpiling..')
    initial_layout = find_best_chain(backend, n1 + n2 + n3)
    circuit_opt = transpile(circuit_opt, backend=backend, routing_method='basic',
                            initial_layout=initial_layout, optimization_level=3)

    print(f'  Done. Ops: {display_nops(circuit_opt)}')
    circuit_opt.name = f'optimized_{n1}_{n2}'

    return [circuit_orig, circuit_opt]
# List of circuits
circuits = []
for n1, n2 in [(4, 4), (3, 3), (2, 2), (1, 1)]:
    circuits += make_circuits(n1, n2, backend)

# バックエンドで定められた最大のショット数だけ各回路を実行
shots = min(backend.max_shots, 2000)

print(f'Submitting {len(circuits)} circuits to {backend.name}, {shots} shots each')

sampler = Sampler(backend)
job = sampler.run(circuits, shots=shots)
counts_list = [result.data.meas.get_counts() for result in job.result()]

ジョブが返ってきたら、正しい足し算を表しているものの割合を調べてみましょう。

def count_correct_additions(counts, n1, n2):
    """Extract the addition equation from the counts dict key and tally up the correct ones."""

    correct = 0

    for key, value in counts.items():
        # cf. plot_counts() from the SIMD lecture
        x1 = int(key[-n1:], 2)
        x2 = int(key[-n1 - n2:-n1], 2)
        x3 = int(key[:-n1 - n2], 2)

        if x1 + x2 == x3:
            correct += value

    return correct


icirc = 0
for n1, n2 in [(4, 4), (3, 3), (2, 2), (1, 1)]:
    for ctype in ['Original', 'Optimized']:
        n_correct = count_correct_additions(counts_list[icirc], n1, n2)
        r_correct = n_correct / shots
        print(f'{ctype} circuit ({n1}, {n2}): {n_correct} / {shots} = {r_correct:.3f} +- {np.sqrt(r_correct * (1. - r_correct) / shots):.3f}')
        icirc += 1

ちなみに、ibm_kawasakiというマシンで同じコードを走らせると、下のような結果が得られます。

Original circuit with n1, n2 = 4, 4
  Transpiling..
  Done. Ops: N(rz)=170, N(x)=3, N(sx)=67, N(cx)=266
Optimized circuit with n1, n2 = 4, 4
  Transpiling..
  Done. Ops: N(rz)=175, N(x)=1, N(sx)=64, N(cx)=142
Original circuit with n1, n2 = 3, 3
  Transpiling..
  Done. Ops: N(rz)=120, N(x)=5, N(sx)=57, N(cx)=90
Optimized circuit with n1, n2 = 3, 3
  Transpiling..
  Done. Ops: N(rz)=117, N(x)=0, N(sx)=48, N(cx)=84
Original circuit with n1, n2 = 2, 2
  Transpiling..
  Done. Ops: N(rz)=50, N(x)=0, N(sx)=20, N(cx)=56
Optimized circuit with n1, n2 = 2, 2
  Transpiling..
  Done. Ops: N(rz)=67, N(x)=0, N(sx)=32, N(cx)=41
Original circuit with n1, n2 = 1, 1
  Transpiling..
  Done. Ops: N(rz)=25, N(x)=0, N(sx)=15, N(cx)=13
Optimized circuit with n1, n2 = 1, 1
  Transpiling..
  Done. Ops: N(rz)=27, N(x)=0, N(sx)=16, N(cx)=13
Original circuit (4, 4): 990 / 32000 = 0.031 +- 0.001
Optimized circuit (4, 4): 879 / 32000 = 0.027 +- 0.001
Original circuit (3, 3): 2435 / 32000 = 0.076 +- 0.001
Optimized circuit (3, 3): 2853 / 32000 = 0.089 +- 0.002
Original circuit (2, 2): 3243 / 32000 = 0.101 +- 0.002
Optimized circuit (2, 2): 7994 / 32000 = 0.250 +- 0.002
Original circuit (1, 1): 25039 / 32000 = 0.782 +- 0.002
Optimized circuit (1, 1): 26071 / 32000 = 0.815 +- 0.002

回路が均一にランダムに\(0\)から\(2^{n_1 + n_2 + n_3} - 1\)までの数を返す場合、レジスタ1と2のそれぞれの値の組み合わせに対して正しいレジスタ3の値が一つあるので、正答率は\(2^{n_1 + n_2} / 2^{n_1 + n_2 + n_3} = 2^{-n_3}\)となります。実機では、(4, 4)と(3, 3)でどちらの回路も正答率がほとんどこの値に近くなっています。(2, 2)では効率化回路で明らかにランダムでない結果が出ています。(1, 1)では両回路とも正答率8割です。

フェイクバックエンドでは実機のエラーもシミュレートされていますが、エラーのモデリングが甘い部分もあり、\(2^{-n_3}\)よりは遥かに良い成績が得られています。いずれのケースも、回路が短い効率化バージョンの方が正答率が高くなっています。