Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Qiskitサポート

タグ: integration optimization variational circuit-compilation

このページでは、具体的な最適化問題を通して、QamomileのQiskit量子SDK連携を紹介します。 QiskitはQamomileの標準の量子SDK連携です。qamomileをインストールすれば、QiskitTranspilerQiskitExecutorをすぐに使えます。 このチュートリアルでは、小さなMaxCutインスタンスに対するQAOA最適化を例に、Qamomileの量子カーネルをQiskit回路へトランスパイルし、Qiskitシミュレータ上でサンプリングと期待値評価を行います。 さらに、Qiskitの高度な回路機能も紹介します。

# 最新のQamomileをpipからインストールします。
# Qiskitとqiskit-aerはコア依存なので、追加の依存グループは不要です。
# !pip install qamomile
import os

import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, depolarizing_error
from scipy.optimize import minimize

import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.optimization.binary_model import BinaryModel
from qamomile.qiskit import QiskitTranspiler

MaxCut問題

Qiskit連携の説明に集中するため、MaxCutに対するQAOAチュートリアルと同じ5ノードの小さなグラフを使います。 (i,j)E(1sisj)/2\sum_{(i,j) \in E}(1 - s_i s_j)/2の最大化は、定数項を除けば、反強磁性IsingハミルトニアンHC=(i,j)EsisjH_C = \sum_{(i,j) \in E} s_i s_jの最小化に対応します。 重みなしのMaxCutでは、すべてのJij=1J_{ij} = 1hi=0h_i = 0なので、これらの係数をそのままBinaryModel.from_isingに渡します。

# MaxCutグラフを作り、Ising形式のBinaryModelへ変換します。
G = nx.Graph()
G.add_edges_from([(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4)])
num_nodes = G.number_of_nodes()

ising_quad: dict[tuple[int, int], float] = {
    tuple(sorted((i, j))): 1.0 for i, j in G.edges()
}
ising_linear: dict[int, float] = {}
spin_model = BinaryModel.from_ising(linear=ising_linear, quad=ising_quad)
# 問題の構造はグラフから一意に決まります。重みなしMaxCutでは、quad項は辺と
# 1対1に対応し、linear項は存在しません。
assert len(spin_model.quad) == G.number_of_edges()
assert len(spin_model.linear) == 0

pos = nx.spring_layout(G, seed=42)
plt.figure(figsize=(5, 4))
nx.draw(
    G,
    pos,
    with_labels=True,
    node_color="white",
    node_size=700,
    edgecolors="black",
)
plt.title(f"MaxCut graph: {num_nodes} nodes, {G.number_of_edges()} edges")
plt.show()
<Figure size 500x400 with 1 Axes>

@qkernelによるQAOAアンザッツの構築

MaxCutを解くためのQAOA回路を@qkernelとして記述します。 レシピはMaxCutに対するQAOAチュートリアルと同じです。 計算基底の一様な重ね合わせ状態を準備した後、コスト層とミキサー層をpp回交互に適用し、最後に計算基底で測定します。

# グラフの全ノードに対応する一様重ね合わせを準備します。
@qmc.qkernel
def superposition(n: qmc.UInt) -> qmc.Vector[qmc.Qubit]:
    q = qmc.qubit_array(n, name="q")
    for i in qmc.range(n):
        q[i] = qmc.h(q[i])
    return q

@qmc.qkernel
def cost_layer(
    quad: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
    linear: qmc.Dict[qmc.UInt, qmc.Float],
    q: qmc.Vector[qmc.Qubit],
    gamma: qmc.Float,
) -> qmc.Vector[qmc.Qubit]:
    for (i, j), Jij in quad.items():
        q[i], q[j] = qmc.rzz(q[i], q[j], angle=Jij * gamma)
    for i, hi in linear.items():
        q[i] = qmc.rz(q[i], angle=hi * gamma)
    return q

@qmc.qkernel
def mixer_layer(
    q: qmc.Vector[qmc.Qubit],
    beta: qmc.Float,
) -> qmc.Vector[qmc.Qubit]:
    n = q.shape[0]
    for i in qmc.range(n):
        q[i] = qmc.rx(q[i], angle=2.0 * beta)
    return q

@qmc.qkernel
def qaoa_state(
    p: qmc.UInt,
    quad: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
    linear: qmc.Dict[qmc.UInt, qmc.Float],
    n: qmc.UInt,
    gammas: qmc.Vector[qmc.Float],
    betas: qmc.Vector[qmc.Float],
) -> qmc.Vector[qmc.Qubit]:
    q = superposition(n)
    for layer in qmc.range(p):
        q = cost_layer(quad, linear, q, gammas[layer])
        q = mixer_layer(q, betas[layer])
    return q

@qmc.qkernel
def qaoa_ansatz(
    p: qmc.UInt,
    quad: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
    linear: qmc.Dict[qmc.UInt, qmc.Float],
    n: qmc.UInt,
    gammas: qmc.Vector[qmc.Float],
    betas: qmc.Vector[qmc.Float],
) -> qmc.Vector[qmc.Bit]:
    q = qaoa_state(p, quad, linear, n, gammas, betas)
    return qmc.measure(q)

@qmc.qkernel
def qaoa_energy(
    p: qmc.UInt,
    quad: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
    linear: qmc.Dict[qmc.UInt, qmc.Float],
    n: qmc.UInt,
    gammas: qmc.Vector[qmc.Float],
    betas: qmc.Vector[qmc.Float],
    H: qmc.Observable,
) -> qmc.Float:
    q = qaoa_state(p, quad, linear, n, gammas, betas)
    return qmc.expval(q, H)

qaoa_ansatz.draw(...)でQamomileの回路図を描画できます。 問題の構造を決める引数(pquadlinearn)に値を渡し、層の数とグラフの構造を回路図に反映させます。 一方、gammas / betasには値を渡さず、後で決めるパラメータとして残します。

p = 3  # QAOAの層数
qaoa_ansatz.draw(
    p=p,
    quad=spin_model.quad,
    linear=spin_model.linear,
    n=num_nodes,
)
<Figure size 1701x416 with 1 Axes>

Qiskitへのトランスパイル

Qamomileの量子カーネルで定義した回路は、QiskitTranspilerでQiskitのQuantumCircuitへトランスパイルできます。 QiskitTranspilerは、他の量子SDKと同じようにtranspile()で使えます。 問題の構造を決める引数はbindingsで固定し、gammas / betasはランタイムパラメータとして残します。 チュートリアルの出力を再現できるように、固定シードとmax_parallel_threads=1を設定したAerSimulatorを使います。

SEED = 42

def make_seeded_backend() -> AerSimulator:
    return AerSimulator(seed_simulator=SEED, max_parallel_threads=1)

transpiler = QiskitTranspiler()

executable = transpiler.transpile(
    qaoa_ansatz,
    bindings={
        "p": p,
        "quad": spin_model.quad,
        "linear": spin_model.linear,
        "n": num_nodes,
    },
    parameters=["gammas", "betas"],
)

executable.get_first_circuit()で内部のQiskitQuantumCircuitを取り出せます。 2p2p個のQAOA角度(gammas[0..p-1]betas[0..p-1])は、実行時までQiskitのParameterオブジェクトとして残ります。 ここでは、トランスパイルされたQuantumCircuitをQiskitのテキストdrawerで確認します。

# 出力された回路を確認し、パラメータが未バインドのまま残っていることを確認します。
qiskit_circuit = executable.get_first_circuit()
assert qiskit_circuit is not None

# QAOAでは、グラフの各ノードに1つの量子ビットと1つの最終古典ビットを使います。
# ランタイムパラメータは、各層にgammaとbetaが1つずつです。
assert qiskit_circuit.num_qubits == num_nodes
assert qiskit_circuit.num_clbits == num_nodes
assert qiskit_circuit.num_parameters == 2 * p
assert set(executable.parameter_names) == {
    *(f"gammas[{i}]" for i in range(p)),
    *(f"betas[{i}]" for i in range(p)),
}
print(type(qiskit_circuit).__name__)
print("num_qubits    :", qiskit_circuit.num_qubits)
print("num_clbits    :", qiskit_circuit.num_clbits)
print("num_parameters:", qiskit_circuit.num_parameters)
print("parameters    :", sorted(str(param) for param in qiskit_circuit.parameters))

print(qiskit_circuit.draw(output="text", fold=120))
QuantumCircuit
num_qubits    : 5
num_clbits    : 5
num_parameters: 6
parameters    : ['betas[0]', 'betas[1]', 'betas[2]', 'gammas[0]', 'gammas[1]', 'gammas[2]']
     ┌───┐                                ┌────────────────┐                                                    »
q_0: ┤ H ├─■───────────────■──────────────┤ Rx(2*betas[0]) ├────────────────────────────────────■───────────────»
     ├───┤ │ZZ(gammas[0])  │              └────────────────┘                ┌────────────────┐  │ZZ(gammas[1])  »
q_1: ┤ H ├─■───────────────┼────────────────■────────────────■──────────────┤ Rx(2*betas[0]) ├──■───────────────»
     ├───┤                 │ZZ(gammas[0])   │ZZ(gammas[0])   │              └────────────────┘┌────────────────┐»
q_2: ┤ H ├─────────────────■────────────────■────────────────┼────────────────■───────────────┤ Rx(2*betas[0]) ├»
     ├───┤                                                   │ZZ(gammas[0])   │ZZ(gammas[0])  └────────────────┘»
q_3: ┤ H ├───────────────────────────────────────────────────■────────────────■─────────────────■───────────────»
     ├───┤                                                                                      │ZZ(gammas[0])  »
q_4: ┤ H ├──────────────────────────────────────────────────────────────────────────────────────■───────────────»
     └───┘                                                                                                      »
c: 5/═══════════════════════════════════════════════════════════════════════════════════════════════════════════»
                                                                                                                »
«                       ┌────────────────┐                                                                      »
«q_0: ──■───────────────┤ Rx(2*betas[1]) ├────────────────────────────────────■─────────────────■───────────────»
«       │               └────────────────┘                ┌────────────────┐  │ZZ(gammas[2])    │               »
«q_1: ──┼─────────────────■────────────────■──────────────┤ Rx(2*betas[1]) ├──■─────────────────┼───────────────»
«       │ZZ(gammas[1])    │ZZ(gammas[1])   │              └────────────────┘┌────────────────┐  │ZZ(gammas[2])  »
«q_2: ──■─────────────────■────────────────┼────────────────■───────────────┤ Rx(2*betas[1]) ├──■───────────────»
«     ┌────────────────┐                   │ZZ(gammas[1])   │ZZ(gammas[1])  └────────────────┘┌────────────────┐»
«q_3: ┤ Rx(2*betas[0]) ├───────────────────■────────────────■─────────────────■───────────────┤ Rx(2*betas[1]) ├»
«     ├────────────────┤                                                      │ZZ(gammas[1])  ├────────────────┤»
«q_4: ┤ Rx(2*betas[0]) ├──────────────────────────────────────────────────────■───────────────┤ Rx(2*betas[1]) ├»
«     └────────────────┘                                                                      └────────────────┘»
«c: 5/══════════════════════════════════════════════════════════════════════════════════════════════════════════»
«                                                                                                               »
«     ┌────────────────┐                ┌─┐                                                                  
«q_0: ┤ Rx(2*betas[2]) ├────────────────┤M├──────────────────────────────────────────────────────────────────
«     └────────────────┘                └╥┘┌────────────────┐                  ┌─┐                           
«q_1: ──■────────────────■───────────────╫─┤ Rx(2*betas[2]) ├──────────────────┤M├───────────────────────────
«       │ZZ(gammas[2])   │               ║ └────────────────┘┌────────────────┐└╥┘                  ┌─┐      
«q_2: ──■────────────────┼───────────────╫───■───────────────┤ Rx(2*betas[2]) ├─╫───────────────────┤M├──────
«                        │ZZ(gammas[2])  ║   │ZZ(gammas[2])  └────────────────┘ ║ ┌────────────────┐└╥┘┌─┐   
«q_3: ───────────────────■───────────────╫───■─────────────────■────────────────╫─┤ Rx(2*betas[2]) ├─╫─┤M├───
«                                        ║                     │ZZ(gammas[2])   ║ ├────────────────┤ ║ └╥┘┌─┐
«q_4: ───────────────────────────────────╫─────────────────────■────────────────╫─┤ Rx(2*betas[2]) ├─╫──╫─┤M├
«                                        ║                                      ║ └────────────────┘ ║  ║ └╥┘
«c: 5/═══════════════════════════════════╩══════════════════════════════════════╩════════════════════╩══╩══╩═
«                                        0                                      1                    2  3  4 

各ランタイムパラメータは、実行時まで未バインドのまま残ります。 gammas / betasのバインドは、ExecutableProgram.sample(...)ExecutableProgram.run(...)からQiskitのassign_parametersを通して行われるため、Qiskit回路を一度生成すれば、多くのパラメータベクトルで再利用できます。 Ising係数、量子ビット数、層数といった問題構造はトランスパイル時に固定され、ランタイム入力として残るのは変分角度だけです。

QiskitExecutorによるQAOAサンプリング

executable.sample(executor, bindings=..., shots=...)SampleJobを返します。 .result()で得られるSampleResultは、BinaryModel.decode_from_sampleresultでスピン変数(+1/1)(+1 / -1)BinarySampleSetへデコードできます。 これにより、追加の変換をせずにカット辺を数えられます。 qiskit-aerがインストールされている環境では、QiskitExecutor()はデフォルトでAerSimulatorを使います。ここでは上で作ったシード付きシミュレータを使います。

rng = np.random.default_rng(SEED)
init_params = rng.uniform(-np.pi / 2, np.pi / 2, 2 * p)
init_gammas = list(init_params[:p])
init_betas = list(init_params[p:])
docs_test_mode = os.environ.get("QAMOMILE_DOCS_TEST") == "1"
sample_shots = 256 if docs_test_mode else 2000
maxiter = 20 if docs_test_mode else 100

# パラメータ化されたexecutableをサンプリングし、ビット列をIsingエネルギーへデコードします。
executor = transpiler.executor(backend=make_seeded_backend())
sample_result = executable.sample(
    executor,
    bindings={"gammas": init_gammas, "betas": init_betas},
    shots=sample_shots,
).result()

decoded = spin_model.decode_from_sampleresult(sample_result)
print(f"Mean energy at random init: {decoded.energy_mean():+.4f}")
assert sample_result.shots == sample_shots
Mean energy at random init: -0.6840

QAOAパラメータの最適化

同じexecutableを異なる(gammas, betas)で繰り返し呼び出すのが、QAOAの最適化ループの基本形です。 transpiler.transpile()を1回呼び、その後はexecutable.sample()を何度も呼び出します。 この例では、サンプリングとデコードの処理をcost_fn()として定義し、SciPyのminimize関数で最適化します。 古典最適化関数は(gammas, betas)を更新しながら、サンプリングされたIsingエネルギーの平均を下げていきます。 各反復では、同じexecutableQiskitExecutorを再利用します。

# 1つのexecutableを古典目的関数の中で再利用します。
cost_history: list[float] = []

def cost_fn(params: np.ndarray) -> float:
    result = executable.sample(
        executor,
        bindings={"gammas": list(params[:p]), "betas": list(params[p:])},
        shots=sample_shots,
    ).result()
    energy = spin_model.decode_from_sampleresult(result).energy_mean()
    cost_history.append(energy)
    return energy

# COBYLAでサンプリング平均エネルギーを最適化します。
res = minimize(cost_fn, init_params, method="COBYLA", options={"maxiter": maxiter})

opt_gammas = list(res.x[:p])
opt_betas = list(res.x[p:])
print(f"Optimized mean energy: {res.fun:+.4f}")
print(f"Optimal gammas       : {[round(float(v), 4) for v in opt_gammas]}")
print(f"Optimal betas        : {[round(float(v), 4) for v in opt_betas]}")
assert cost_history
Optimized mean energy: -2.8810
Optimal gammas       : [0.8678, -0.4364, 1.562]
Optimal betas        : [0.4086, -0.8709, 2.9557]
# 最適化の過程における目的関数の変化をプロットします。
plt.figure(figsize=(8, 4))
plt.plot(cost_history, color="#2696EB")
plt.xlabel("Iteration")
plt.ylabel("Cost (mean Ising energy)")
plt.title("QAOA optimization on Aer via `QiskitExecutor`")
plt.tight_layout()
plt.show()
<Figure size 800x400 with 1 Axes>

上のAerSimulatorは固定のseed_simulatorで構築しているため、同じ回路列に対して再現可能なサンプリング列が得られます。 この5ノードグラフでは、HCH_Cの基底状態エネルギー付近まで収束するはずです。 ここで得た最適パラメータ(opt_gammasopt_betas)を、以降の例でも使います。

run()による期待値計算

Qamomileでは、量子回路の出力に対する期待値を量子カーネル内のqmc.expval(...)で記述します。 これをQiskitへトランスパイルすると、ExecutableProgram.run(executor, bindings=...)で呼び出せる実行可能オブジェクトになります。 run()はQamomileのパラメータ情報を使ってランタイムパラメータをバインドし、そのうえでQiskitのestimatorを呼び出します。

まずHC=(i,j)EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_jをQamomileのHamiltonianとして組み立てます。 その後、期待値計算用の量子カーネルをトランスパイルし、最適化済みQAOAパラメータで評価します。

# MaxCutのIsingコストに対応するQamomile Hamiltonianを組み立てます。
cost_hamiltonian = qm_o.Hamiltonian()
for (i, j), Jij in spin_model.quad.items():
    cost_hamiltonian.add_term(
        (qm_o.PauliOperator(qm_o.Pauli.Z, i), qm_o.PauliOperator(qm_o.Pauli.Z, j)),
        Jij,
    )
for i, hi in spin_model.linear.items():
    cost_hamiltonian.add_term((qm_o.PauliOperator(qm_o.Pauli.Z, i),), hi)

# 期待値計算用の量子カーネルをトランスパイルし、`run()`で評価します。
expval_executable = transpiler.transpile(
    qaoa_energy,
    bindings={
        "p": p,
        "quad": spin_model.quad,
        "linear": spin_model.linear,
        "n": num_nodes,
        "H": cost_hamiltonian,
    },
    parameters=["gammas", "betas"],
)
energy_via_run = expval_executable.run(
    executor,
    bindings={"gammas": opt_gammas, "betas": opt_betas},
).result()

print(f"Executable.run() expectation: {energy_via_run:+.10f}")
print(f"sample mean energy          : {res.fun:+.4f}")
assert np.isfinite(energy_via_run)
Executable.run() expectation: -2.8538574303
sample mean energy          : -2.8810

QamomileのAPIだけで扱う場合は、ExecutableProgram.run(...)を使うのがおすすめです。 Qiskit回路を自分で扱いたい場合にはexecutor.estimate(...)も使えますが、その場合はQiskitのパラメータ順や回路のバインド状態をユーザー側で管理する必要があります。 QiskitExecutorは、利用可能な場合にはデフォルトでQiskitのStatevectorEstimatorを生成するため、現在のQiskit環境ではV2 primitiveインターフェースを使います。 カスタムestimatorや古いQiskit / AerのestimatorがV2形式のrun([(circuit, observable, params)])呼び出しを受け付けない場合、QamomileはV1形式のrun(circuits, observables, parameter_values)へフォールバックします。

Qiskitの高度な機能

Qamomileでは、Qiskitを標準の量子SDK連携として使えます。 そのため、Qiskitが持つ高度な回路機能を活用するための入口も用意しています。

このセクションでは、生成した回路をQiskitの実行対象へ渡すときに便利な機能を3つ示します。

  • 動的回路のためのネイティブ古典制御フロー(for_loopif_elsewhile_loop)

  • パラメトリックな時間発展qmc.pauli_evolve(...)をQiskitネイティブなPauliEvolutionGateとして直接出力

  • QamomileのコンポジットQFT操作向けのネイティブQFTGate / 逆QFTGate

古典制御フローとランタイム古典式

Qiskit連携は、Qamomileの古典制御フローやランタイム古典式を、Qiskitの動的回路命令や古典式に直接変換できます。 qmc.range(...)ループは、Qiskitのfor_loopになります。 測定結果に基づくif / elsewhileは、Qiskitの動的回路命令になります。 a & bのような条件式は、qiskit.circuit.classical.exprを通してQiskitの古典式に直接変換できます。

# ネイティブ制御フローの出力を確認する小さな量子カーネルを3つ定義します。

# qmc.rangeによるforループは、Qiskitの`for_loop`になります。
@qmc.qkernel
def native_for_demo(reps: qmc.UInt) -> qmc.Bit:
    q = qmc.qubit("q")
    for _ in qmc.range(reps):
        q = qmc.h(q)
    return qmc.measure(q)

# 測定結果に基づくif分岐は、Qiskitの`if_else`になります。
@qmc.qkernel
def runtime_branch_demo() -> qmc.Bit:
    a = qmc.qubit("a")
    b = qmc.qubit("b")
    target = qmc.qubit("target")

    a = qmc.x(a)
    b = qmc.x(b)
    ma = qmc.measure(a)
    mb = qmc.measure(b)

    if ma & mb:
        target = qmc.x(target)
    else:
        target = qmc.h(target)

    return qmc.measure(target)

# 測定結果に基づくwhileループは、Qiskitの`while_loop`になります。
@qmc.qkernel
def repeat_until_zero_once() -> qmc.Bit:
    q0 = qmc.qubit("q0")
    q0 = qmc.x(q0)
    bit = qmc.measure(q0)
    while bit:
        q1 = qmc.qubit("q1")
        bit = qmc.measure(q1)
    return bit

# 各demoをトランスパイルし、出力されたQiskitの操作名を確認します。
for_circuit = transpiler.to_circuit(native_for_demo, bindings={"reps": 3})
branch_circuit = transpiler.to_circuit(runtime_branch_demo)
while_circuit = transpiler.to_circuit(repeat_until_zero_once)

for_ops = [inst.operation.name for inst in for_circuit.data]
branch_ops = [inst.operation.name for inst in branch_circuit.data]
while_ops = [inst.operation.name for inst in while_circuit.data]

print("native_for_demo ops       :", for_ops)
print("runtime_branch_demo ops   :", branch_ops)
print("repeat_until_zero_once ops:", while_ops)
assert "for_loop" in for_ops
assert "if_else" in branch_ops
assert "while_loop" in while_ops

if_op = next(inst.operation for inst in branch_circuit.data if inst.operation.name == "if_else")
print("if_else condition:", if_op.condition)
native_for_demo ops       : ['for_loop', 'measure']
runtime_branch_demo ops   : ['x', 'x', 'measure', 'measure', 'if_else', 'measure']
repeat_until_zero_once ops: ['x', 'measure', 'while_loop']
if_else condition: Binary(Binary.<Op.LOGIC_AND: 4>, Var(<Clbit register=(3, "c"), index=0>, Bool()), Var(<Clbit register=(3, "c"), index=1>, Bool()), Bool())

Qiskitの古典式システムは現在、Qamomileが扱うことのできる多くの論理演算、比較演算、算術演算に対応しています。 ただし、FLOORDIVPOWには対応するQiskitの古典式がないため、どちらかが回路実行時に評価する式として残ると、Qamomileは回路生成時にNotImplementedErrorを発生させます。 これらが必要な場合は、トランスパイル前に具体値として決まる形にしてください。

ネイティブPauliEvolutionGate

qmc.pauli_evolve(q, H, gamma)は、Qamomileの中間表現ではeiγHe^{-i\gamma H}を表します。 Qiskit連携は、use_native_composite=True(デフォルト)の場合、この操作をPauliEvolutionGateとして出力します。 未バインドのgammaはQiskitのParameterになるため、同じ回路を変分パラメータを変えながら評価する用途に再利用できます。

# Pauli発展の量子カーネルを出力し、Qiskitでネイティブ表現が保たれることを確認します。
@qmc.qkernel
def pauli_evolve_demo(
    n: qmc.UInt,
    H: qmc.Observable,
    gamma: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
    # Hamiltonianによる時間発展を適用する前に、単純な入力状態を準備します。
    q = qmc.qubit_array(n, "q")
    for i in qmc.range(n):
        q[i] = qmc.h(q[i])
    q = qmc.pauli_evolve(q, H, gamma)
    return qmc.measure(q)

# 出力された操作を確認しやすいよう、小さなHamiltonianを使います。
evolution_hamiltonian = qm_o.Z(0) * qm_o.Z(1) + 0.5 * qm_o.X(0)
evolution_executable = transpiler.transpile(
    pauli_evolve_demo,
    bindings={"n": evolution_hamiltonian.num_qubits, "H": evolution_hamiltonian},
    parameters=["gamma"],
)
evolution_circuit = evolution_executable.get_first_circuit()
assert evolution_circuit is not None
evolution_ops = [inst.operation.name for inst in evolution_circuit.data]
print(evolution_ops)
assert "PauliEvolution" in evolution_ops
assert {str(param) for param in evolution_circuit.parameters} == {"gamma"}
['h', 'h', 'PauliEvolution', 'measure', 'measure']

量子SDKに依存しないゲート分解を確認したい場合は、QiskitTranspiler(use_native_composite=False)を渡します。 同じフラグでネイティブQFT/IQFT出力も無効化できるため、デバッグや量子SDK非依存のゲート数比較に便利です。

ネイティブQFTGate

Qamomileには、QFTや逆QFTをqmc.qft(...) / qmc.iqft(...)で表す高水準の操作があります。 Qiskit連携では、これらの量子カーネルを量子ゲートへ分解せず、QiskitネイティブなQFTGateとして直接出力できます。 量子ゲートに分解された回路が必要な場合は、use_native_composite=Falseを指定すると、H/controlled-phase/SWAPに展開されます。

# QiskitのネイティブQFTゲートと、ゲート分解された回路を比較します。
@qmc.qkernel
def qft_demo(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, "q")
    q = qmc.qft(q)
    return qmc.measure(q)


qft_native = QiskitTranspiler(use_native_composite=True).to_circuit(
    qft_demo,
    bindings={"n": 3},
)
qft_decomposed = QiskitTranspiler(use_native_composite=False).to_circuit(
    qft_demo,
    bindings={"n": 3},
)
native_ops = [inst.operation.name for inst in qft_native.data]
decomposed_ops = [inst.operation.name for inst in qft_decomposed.data]
print("native QFT ops    :", native_ops)
print("decomposed QFT ops:", decomposed_ops)
assert any("qft" in name.lower() for name in native_ops)
assert "cp" in decomposed_ops
assert len(qft_native.data) < len(qft_decomposed.data)
native QFT ops    : ['qft', 'measure', 'measure', 'measure']
decomposed QFT ops: ['h', 'cp', 'cp', 'h', 'cp', 'h', 'swap', 'measure', 'measure', 'measure']

他のQiskit実行対象の利用

QiskitExecutorでは、トランスパイル済み回路と、それを実行するQiskitの実行対象を分けて扱います。 そのため、transpiler.executor(backend=...)でQiskitの実行対象を差し替えるだけで、同じ回路をさまざまなQiskit実行対象で実行できます。 例えば、ノイズなしのローカルシミュレータやAerノイズモデルに加えて、IBM Quantumが提供する実機も利用できます。

ここでは、脱分極ノイズを持つAerノイズモデルを作り、AerSimulatorへ渡す例を示します。 同じ最適化済みパラメータで、ノイズなしとノイズありのサンプル平均エネルギーを比較します。

# 1量子ビットゲートと2量子ビットゲートの脱分極ノイズを持つAerノイズモデルを作ります。
noise_model = NoiseModel()
one_qubit_error = depolarizing_error(0.01, 1)
two_qubit_error = depolarizing_error(0.02, 2)
noise_model.add_all_qubit_quantum_error(one_qubit_error, ["h", "rx", "rz"])
noise_model.add_all_qubit_quantum_error(two_qubit_error, ["rzz"])

noisy_backend = AerSimulator(
    noise_model=noise_model,
    seed_simulator=SEED,
    max_parallel_threads=1,
)
noisy_executor = transpiler.executor(backend=noisy_backend)

# 同じexecutableを、ノイズなしとノイズありの実行対象で実行します。
clean_result = executable.sample(
    executor,
    bindings={"gammas": opt_gammas, "betas": opt_betas},
    shots=sample_shots,
).result()
noisy_result = executable.sample(
    noisy_executor,
    bindings={"gammas": opt_gammas, "betas": opt_betas},
    shots=sample_shots,
).result()

# 両方のサンプル集合をデコードし、平均Isingエネルギーを比較します。
clean_energy = spin_model.decode_from_sampleresult(clean_result).energy_mean()
noisy_energy = spin_model.decode_from_sampleresult(noisy_result).energy_mean()
print(f"noiseless Aer mean energy: {clean_energy:+.4f}")
print(f"noisy Aer mean energy: {noisy_energy:+.4f}")
assert clean_result.shots == sample_shots
assert noisy_result.shots == sample_shots
assert np.isfinite(clean_energy)
assert np.isfinite(noisy_energy)
noiseless Aer mean energy: -2.8810
noisy Aer mean energy: -2.1370

まとめ

  • QiskitTranspiler().transpile(kernel, bindings=..., parameters=[...])は量子カーネルをExecutableProgram[QuantumCircuit]に変換します。Qiskitエコシステム内で扱いたい場合は、to_circuit(...)で生のQiskitQuantumCircuitを取得できます。

  • QiskitExecutorは、測定を返す量子カーネル向けのexecutable.sample()と、期待値向けのexecutable.run() / executor.estimate(...)の両方をサポートします。デフォルトではAerSimulatorを使い、transpiler.executor(backend=...)から任意のQiskit実行対象オブジェクトを受け取れます。

  • Qiskit連携は、Qiskitが高い抽象度の回路命令を持つ箇所では、回路途中の測定、動的for_loop / if_else / while_loop、ランタイム古典式、PauliEvolutionGateQFTGateをネイティブに出力します。

  • Aerノイズモデル、providerが提供する実行対象、qBraidでラップしたQiskitデバイスを、qkernelを再トランスパイルせずに使えます。qamomile.optimizationのヘルパーも、同じQiskit回路を受け渡す仕組みを使っています。