Skip to main content
IBM Quantum Platform

格子ハミルトニアンのクリロフ量子対角化

所要時間の目安:Heron または Nighthawk プロセッサで 70 分(注:これはあくまで目安です。 (実行時間は状況によって異なる場合があります。)


学習成果

  • クリロフ量子対角化(KQD)を、スペクトルフィルタとして機能する有限ハミルトニアン関数の学習としてどのように解釈すべきか。
  • 拡張スワップテスト測定を用いて、投影ハミルトニアンおよびオーバーラップ行列を構築する方法。
  • 得られた一般化固有値問題(GEVP)を解き、格子ハミルトニアンの基底状態エネルギーの推定値を導出する方法。

前提条件


背景

このチュートリアルでは、Qiskitのパターン機能を活用して、クリロフ量子対角化(KQD)アルゴリズムを実装する方法を解説します。 まず、アルゴリズムの理論について学び、その後、QPU上での実行デモをご覧いただきます。

多体ハミルトニアンの低エネルギー特性を推定することは、量子シミュレーションにおける中心的な課題である。 例えば、基底状態のエネルギーや低エネルギー励起は、化学的安定性、磁気秩序、量子相転移、および物質の応答と直接的な関係がある。 従来のコンピュータでは、ヒルベルト空間の次元は軌道数やスピン数に応じて指数関数的に増加するため、直接対角化はすぐに現実的ではなくなってしまう。

この問題に対しては、いくつかの量子コンピューティングによるアプローチが存在する。 変分量子固有値ソルバー(VQE) などの近視眼的な変分法は、比較的浅いパラメータ化回路を使用するが、多くの量子回路の評価を伴う非線形な古典的最適化ループを必要とする。 一方、 量子位相推定(QPE) は、厳密な保証のもとで固有値推定を行うより直接的な手法を提供するが、標準的なQPEには長いコヒーレント回路が必要であり、主に耐故障性量子コンピュータに適している。 KQDは、これら2つのアプローチの中間に位置する。位相推定に基づくアルゴリズムと同様にリアルタイムのハミルトニアン進化を用いるが、完全な位相推定の代わりに、古典的に解くことができるコンパクトな射影固有値問題を適用する。

nn -量子ビットのハミルトニアン HH と、基準状態 ∣ψ0⟩\lvert \psi_{0}\rangle を考える。 KQD法では、リアルタイムで進化させた状態からクリロフ部分空間を構築し、

∣ψℓ⟩=e−iℓΔtH∣ψ0⟩,ℓ=0,1,…,r−1,\begin{equation*} \lvert \psi_\ell\rangle = e^{-i\ell\Delta t H}\lvert \psi_{0}\rangle, \qquad \ell = 0,1,\ldots,r-1, \end{equation*}

ここで、 rr はクリロフ次元、 Δt\Delta t は時間ステップである。 したがって、クリロフ部分空間内の任意の状態は、これらの基底状態の線形結合として表され、

∣ψ(c)⟩=∑ℓ=0r−1cℓ∣ψℓ⟩∥∑ℓ=0r−1cℓ∣ψℓ⟩∥,\begin{equation*} \lvert \psi(\mathbf{c})\rangle = \frac{\sum_{\ell=0}^{r-1} c_\ell \lvert \psi_\ell\rangle} {\left\|\sum_{\ell=0}^{r-1} c_\ell \lvert \psi_\ell\rangle\right\|}, \end{equation*}

ここで、分母は状態を正規化する。

簡単な代数計算により、対応するエネルギーはレイリー商として表されることがわかります

E(c)=⟨ψ(c)∣H∣ψ(c)⟩=∑k,ℓck∗cℓ⟨ψk∣H∣ψℓ⟩∑k,ℓck∗cℓ⟨ψk∣ψℓ⟩=c†Hcc†Sc.\begin{equation*} E(\mathbf{c}) =\langle \psi(\mathbf{c})|H|\psi(\mathbf{c}) \rangle= \frac{ \sum_{k,\ell} c_k^* c_\ell \langle \psi_k\vert H\vert\psi_\ell\rangle }{ \sum_{k,\ell} c_k^* c_\ell \langle \psi_k\vert\psi_\ell\rangle } = \frac{ \mathbf{c}^{\dagger}\mathcal{H}\mathbf{c} }{ \mathbf{c}^{\dagger}\mathcal{S}\mathbf{c} }. \end{equation*}

ここで、行列 S\mathcal{S} および H\mathcal{H} は、

Skℓ=⟨ψk∣ψℓ⟩,Hkℓ=⟨ψk∣H∣ψℓ⟩\begin{equation*} \mathcal{S}_{k\ell}=\langle \psi_k\vert\psi_\ell\rangle, \qquad \mathcal{H}_{k\ell}=\langle \psi_k\vert H\vert\psi_\ell\rangle \end{equation*}

予測オーバーラップ行列およびハミルトニアン行列を定義する。 それらの値は、量子回路の測定を用いて推定される。

E(c)E(\mathbf{c}) を最小にする係数 c\mathbf{c} を求めることを目的とする:

min⁡c≠0E(c).\begin{equation*} \min_{\mathbf{c}\neq \mathbf{0}} E(\mathbf{c}). \end{equation*}

レイリー・リッツの定理によれば、この最小化は一般化固有値問題(GEVP)を解くことと同値であり、

Hc=ESc.\begin{equation*} \mathcal{H}\mathbf{c}=E \mathcal{S}\mathbf{c}. \end{equation*}

なお、 rr の次元は、古典コンピュータがGEVPを解くのに十分なほど小さい場合があることに注意してください。

これは、古典的な部分空間対角化で用いられるのと同じ変分原理ですが、ここでは基底状態が量子時間発展によって生成されます。 VQEと比較して、KQDはリアルタイムの進化に依存しているため、通常、より深い回路を必要とします。 その一方で、KQDは非線形パラメータの最適化や反復的な量子ハードウェアによる実行を回避し、射影される部分空間が拡大するにつれて体系的に性能が向上する。 このアルゴリズムは、既存の量子ハードウェア上で大規模に実証されており [2]、その性能は証明可能な保証のもとで分析することができる [1]。


要件

このチュートリアルを始める前に、以下のものがインストールされていることを確認してください:

  • Qiskit SDK v2.3 またはそれ以降のバージョン( 可視化機能をサポートしたもの)
  • Qiskit Runtime v0.22 またはそれ以降 (pip install qiskit-ibm-runtime)
  • SciPy (pip install scipy)
  • Matplotlib (pip install matplotlib)
  • パンダ (pip install pandas)

ハードウェアでの実行には、 IBM Quantum®qiskit-ibm-runtime アカウントへのアクセス権が必要です。


セットアップ

セットアップセルでは、必要なモジュールをインポートし、ワークフロー用のヘルパー関数を定義しています:

  1. ハイゼンベルク・ハミルトニアンを構築する;
  2. 閾値処理されたGEVPを解く;
  3. 学習済みのクリロフフィルタを評価する;
  4. フィルタの値をスペクトル重みに変換する;
  5. 基準のエネルギー分布とフィルタ処理後のエネルギー分布をプロットする。
from __future__ import annotations

import warnings

import numpy as np
import pandas as pd
import scipy.linalg as la
import matplotlib.pyplot as plt

from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import Operator, SparsePauliOp
from qiskit.synthesis import LieTrotter, SuzukiTrotter
from qiskit.transpiler import PassManager, Layout
from qiskit.transpiler.passes import CommutativeOptimization
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2, Batch
from qiskit_ibm_runtime.fake_provider import FakeMarrakesh

warnings.filterwarnings("ignore")


def make_heisenberg_hamiltonian(
    num_qubits: int,
    coupling: float = 1.0,
) -> SparsePauliOp:
    """Make a Heisenberg Hamiltonian for a 1D chain of qubits with nearest-neighbor interactions."""
    terms: list[tuple[str, complex]] = []

    def append_term(q0: int, q1: int, pauli: str):
        label = ["I"] * num_qubits
        label[num_qubits - 1 - q0] = pauli[0]
        label[num_qubits - 1 - q1] = pauli[1]
        terms.append(("".join(label), coupling))

    for pauli in ("XX", "YY", "ZZ"):
        for q in range(num_qubits - 1):
            append_term(q, q + 1, pauli)
    return SparsePauliOp.from_list(terms).simplify()


def _basis_state_transition_amplitude_sparse(
    hamiltonian: SparsePauliOp,
    bra_state: int,
    ket_state: int,
) -> complex:
    """Evaluate <bra_state|H|ket_state> for computational-basis states."""
    num_qubits = hamiltonian.num_qubits
    amplitude = 0.0 + 0.0j
    for pauli, coeff in zip(hamiltonian.paulis, hamiltonian.coeffs):
        new_state = ket_state
        phase = 1.0 + 0.0j
        for q in range(num_qubits):
            x = bool(pauli.x[q])
            z = bool(pauli.z[q])
            if not x and not z:
                continue
            bit = (new_state >> q) & 1
            if x and z:
                # Y|0> = i|1>, Y|1> = -i|0>
                phase *= 1j if bit == 0 else -1j
                new_state ^= 1 << q
            elif x:
                new_state ^= 1 << q
            else:
                # Z|0> = |0>, Z|1> = -|1>
                if bit:
                    phase *= -1
        if new_state == bra_state:
            amplitude += coeff * phase
    return amplitude


def basis_state_expectation_sparse(
    hamiltonian: SparsePauliOp,
    bitstring: str,
) -> complex:
    """Evaluate <bitstring|H|bitstring>."""
    state = int(bitstring, 2)
    return _basis_state_transition_amplitude_sparse(
        hamiltonian,
        bra_state=state,
        ket_state=state,
    )


def diagonalize_single_1_subspace(
    hamiltonian: SparsePauliOp,
) -> np.ndarray:
    """Diagonalize the Hamiltonian projected onto the single-excitation subspace."""
    num_qubits = hamiltonian.num_qubits

    # Integer basis states |...010...>, with the excitation at qubit k.
    basis = [1 << k for k in range(num_qubits)]

    h_single = np.empty((num_qubits, num_qubits), dtype=complex)

    for row, bra_state in enumerate(basis):
        for col, ket_state in enumerate(basis):
            h_single[row, col] = _basis_state_transition_amplitude_sparse(
                hamiltonian,
                bra_state=bra_state,
                ket_state=ket_state,
            )
    # Remove floating-point-level asymmetry.
    h_single = 0.5 * (h_single + h_single.conj().T)
    evals, _ = np.linalg.eigh(h_single)
    return np.real(evals)


def simple_transpilation(circuit: QuantumCircuit) -> QuantumCircuit:
    """Transpilation to simplify the circuit"""
    pm = PassManager(
        [
            CommutativeOptimization(),
        ]
    )
    circuit = transpile(circuit, optimization_level=3)
    circuit = pm.run(circuit)
    return circuit


def summarize_circuit(circuit: QuantumCircuit) -> dict[str, int | str]:
    """Summarize the circuit with depth, size, and 2-qubit gate information."""
    two_qubit_total = sum(
        inst.operation.num_qubits == 2 for inst in circuit.data
    )
    two_qubit_depth = circuit.depth(lambda x: x[0].num_qubits == 2)
    return {
        "depth": circuit.depth(),
        "size": circuit.size(),
        "2q gates": two_qubit_total,
        "2q depth": two_qubit_depth,
    }


def solve_thresholded_gevp(
    h_matrix: np.ndarray,
    s_matrix: np.ndarray,
    threshold: float = 1e-10,
) -> tuple[float, np.ndarray, int]:
    """Solve H c = E S c using canonical orthogonalization of S."""
    s_vals, s_vecs = la.eigh(s_matrix)

    valid = s_vals > threshold
    if not np.any(valid):
        raise ValueError(
            "All overlap eigenvalues were removed by thresholding."
        )

    keep = valid

    orthogonalizer = s_vecs[:, keep] @ np.diag(1.0 / np.sqrt(s_vals[keep]))
    h_orth = orthogonalizer.conj().T @ h_matrix @ orthogonalizer
    h_orth = 0.5 * (h_orth + h_orth.conj().T)

    eigvals, eigvecs = la.eigh(h_orth)

    coeffs = orthogonalizer @ eigvecs[:, 0]
    normalization = np.sqrt(np.real(coeffs.conj().T @ s_matrix @ coeffs))
    coeffs /= normalization

    return float(np.real(eigvals[0])), coeffs, int(np.sum(keep))

このチュートリアルの前半では、局所状態ベクトルシミュレータを用いて、KQD法を解説します。 その後、実用規模の問題に取り組むために、実際の量子バックエンドを使用します。

また、バックエンド固有のトランスパイルを実証し、その結果として得られる回路を検証するために、架空のバックエンドも定義します。

try:
    service = QiskitRuntimeService()
except Exception:
    QiskitRuntimeService.save_account(
        token="<api_token>", instance="<instance>", overwrite=True
    )
    service = QiskitRuntimeService()

backend = FakeMarrakesh()

小規模シミュレータの例

ステップ1:古典的な入力を量子問題にマッピングする

ハミルトニアンと基準状態

この例では、12キュービットの開放境界ハイゼンベルグ鎖( n=12n=12 )を使用しています

H=∑i=0n−2(XiXi+1+YiYi+1+ZiZi+1),\begin{equation*} H=\sum_{i=0}^{n-2}\left(X_iX_{i+1}+Y_iY_{i+1}+Z_iZ_{i+1}\right), \end{equation*}

単一励起生成物状態を伴う

∣ψ0⟩=∣000001000000⟩\begin{equation*} |\psi_{0}\rangle=|000001000000\rangle \end{equation*}

基準状態として。 上記で定義したハイゼンベルク・ハミルトニアンは励起の総数を保存するため、基準状態は単一励起部分空間にとどまり、その次元は量子ビットの数に比例してのみ増加する。 したがって、その部分空間に制限されたハミルトニアンを対角化することで、基底状態のエネルギーを効率的に正確に計算することができ、これをKQD推定値の診断用ベンチマークとしてのみ利用することができる。 KQDワークフロー自体は、 Qiskit primitives を用いて投影行列要素を推定し、その結果として得られる投影問題を古典的に解きます。

# Problem definition for the simulator example
num_qubits = 12
hamiltonian = make_heisenberg_hamiltonian(num_qubits=num_qubits, coupling=1.0)
ref_bitstring = "000001000000"
ref_energy = basis_state_expectation_sparse(hamiltonian, ref_bitstring)

print("Hamiltonian:")
print(hamiltonian)
print(f"Reference state: |{ref_bitstring}>")
print("Reference energy: ", ref_energy)

Output:

Hamiltonian:
SparsePauliOp(['IIIIIIIIIIXX', 'IIIIIIIIIXXI', 'IIIIIIIIXXII', 'IIIIIIIXXIII', 'IIIIIIXXIIII', 'IIIIIXXIIIII', 'IIIIXXIIIIII', 'IIIXXIIIIIII', 'IIXXIIIIIIII', 'IXXIIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIIYYI', 'IIIIIIIIYYII', 'IIIIIIIYYIII', 'IIIIIIYYIIII', 'IIIIIYYIIIII', 'IIIIYYIIIIII', 'IIIYYIIIIIII', 'IIYYIIIIIIII', 'IYYIIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIIZZI', 'IIIIIIIIZZII', 'IIIIIIIZZIII', 'IIIIIIZZIIII', 'IIIIIZZIIIII', 'IIIIZZIIIIII', 'IIIZZIIIIIII', 'IIZZIIIIIIII', 'IZZIIIIIIIII', 'ZZIIIIIIIIII'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Reference state: |000001000000>
Reference energy:  (7+0j)

アルゴリズムのパラメータを設定する

ハミルトニアンノルムの上界に基づき、参考文献 [1] では、時間ステップ Δt\Delta t を π/∥H∥\pi/\|H\| と経験的に提案している。 スペクトルノルム ∥H∥\|H\| の計算は困難であるため、代わりにその上界を用いる:

∥H∥≤∑i=0n−2∥XiXi+1+YiYi+1+ZiZi+1∥⏟4×4 matrix, easy to calculate the norm=3(n−1).\begin{equation*} \|H\| \le \sum_{i=0}^{n-2}\underbrace{\|X_i X_{i+1}+Y_{i} Y_{i+1}+Z_{i} Z_{i+1}\|}_{4 \times 4 \text{ matrix, easy to calculate the norm}} = 3(n-1). \end{equation*}

クリロフ次元を r=10r=10、1時間ステップあたりのトロッターステップ数を 55 に設定した。これは、最も深い回路( tmax⁡=(r−1)Δtt_{\max}=(r-1)\Delta t )の計算コストを抑えつつ、低エネルギースペクトルを解けるだけの十分な大きさのクリロフ空間を確保し、かつその最も深い回路において離散化誤差を小さく保つのに十分なトロッターステップ数を確保するためである。

dt = np.pi / (3 * (num_qubits - 1))
print("dt in Krylov basis:  ", dt)

krylov_dim = 10
num_trotter_steps = 5

Output:

dt in Krylov basis:   0.09519977738150888

回路を組み立てる

ここでは、行列要素 Hkℓ\mathcal{H}_{k\ell} および Skℓ\mathcal{S}_{k\ell} を推定するための回路を構築する。 HH のすべての冪は可換であるため、次のように表される。

Hkℓ=⟨ψ0∣He−i(ℓ−k)ΔtH∣ψ0⟩=H0,ℓ−k,Skℓ=⟨ψ0∣e−i(ℓ−k)ΔtH∣ψ0⟩=S0,ℓ−k.\begin{equation*} \mathcal{H}_{k\ell} = \langle\psi_{0}|H e^{-i(\ell-k)\Delta tH}|\psi_{0}\rangle=\mathcal{H}_{0,\ell-k}, \qquad \mathcal{S}_{k\ell} = \langle\psi_{0}|e^{-i(\ell-k)\Delta tH}|\psi_{0}\rangle=\mathcal{S}_{0,\ell-k}. \end{equation*}

このように、要素が添字の差に依存する行列はトープリッツ行列と呼ばれ、 d=ℓ−kd=\ell-k でインデックス付けされた第1行の要素から再構成することができます。

ここでは、「 拡張スワップテスト 」と呼ばれる回路を紹介する。この回路は、

∣Φ0d⟩=∣0⟩∣ψ0⟩+∣1⟩∣ψd⟩2,\begin{equation*} |\Phi_{0d}\rangle= \frac{|0\rangle|\psi_0\rangle+|1\rangle|\psi_d\rangle}{\sqrt{2}}, \end{equation*}

ここで、 ∣ψd⟩=e−idΔtH∣ψ0⟩|\psi_d\rangle=e^{-id\Delta tH}|\psi_{0}\rangle である。

参照状態

基準状態 ∣ψ0⟩|\psi_0\rangle を準備する。

qc_ref = QuantumCircuit(num_qubits)
for i, b in enumerate(reversed(ref_bitstring)):
    if b == "1":
        qc_ref.x(i)
display(qc_ref.draw("mpl", scale=0.5))

Output:

Output of the previous code cell

時間発展

単純なリー・トロッター化によって近似されたハミルトニアンによって生成される時間発展演算子を導出する。

t = Parameter("t")

evol_gate = PauliEvolutionGate(
    hamiltonian,
    time=t,
    synthesis=LieTrotter(reps=num_trotter_steps),
    label="U(t)",
)

# Synthesize U(t) first and then control the synthesized circuit.
# This makes the controlled structure visible in the circuit drawer.
evolution_circuit = QuantumCircuit(num_qubits, name="U(t)")
evolution_circuit.append(evol_gate, range(num_qubits))
evolution_circuit = simple_transpilation(evolution_circuit)

# Make a controlled version of the evolution circuit.
controlled_evolution_gate = evolution_circuit.to_gate(label="U(t)").control(
    1, label="C-U(t)"
)
display(
    evolution_circuit.assign_parameters({t: 1.5}).draw(
        "mpl", scale=0.5, fold=-1
    )
)

Output:

Output of the previous code cell

拡張スワップ試験回路 [3]

この回路は、アンシラが ∣0⟩|0\rangle の状態にある間に、まずシステムレジスタに基準状態を設定します:

∣0⟩∣0⟩⊗n⟶∣0⟩∣ψ0⟩.\begin{equation*} |0\rangle |0\rangle^{\otimes n} \longrightarrow |0\rangle |\psi_{0}\rangle . \end{equation*}

次に、アンシラにアダマールゲートを適用すると、2つの分岐のコヒーレントな重ね合わせが生成される:

∣0⟩∣ψ0⟩⟶∣0⟩+∣1⟩2∣ψ0⟩=∣0⟩∣ψ0⟩+∣1⟩∣ψ0⟩2.\begin{equation*} |0\rangle |\psi_0\rangle \longrightarrow \frac{|0\rangle + |1\rangle}{\sqrt{2}} |\psi_0\rangle = \frac{|0\rangle|\psi_0\rangle + |1\rangle|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

最後に、制御された時間発展ゲートが適用されます:

U(t=dΔt)=e−idΔtH\begin{equation*} U(t=d\Delta t) = e^{-id\Delta t H} \end{equation*}

ancillaが ∣1⟩|1\rangle ブランチにある場合のみ。 したがって、

∣0⟩∣ψ0⟩+∣1⟩∣ψ0⟩2⟶∣0⟩∣ψ0⟩+∣1⟩U(dΔt)∣ψ0⟩2.\begin{equation*} \frac{|0\rangle|\psi_0\rangle + |1\rangle|\psi_0\rangle}{\sqrt{2}} \longrightarrow \frac{|0\rangle|\psi_0\rangle + |1\rangle U(d\Delta t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

次のコードブロックでは、以下を実装します:

∣Ψ(t)⟩=∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2,\begin{equation*} |\Psi(t)\rangle = \frac{|0\rangle|\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}}, \end{equation*}

これは、実行ステップにおいて、 d=1,⋯r−1d=1,\cdots r-1 に対して t=dΔtt=d\Delta t として割り当てられます。

ancilla = 0
system_qubits = list(range(1, num_qubits + 1))

extended_swap_test = QuantumCircuit(num_qubits + 1)

# Append state preparation part
extended_swap_test = extended_swap_test.compose(qc_ref, system_qubits)

# Prepare the coherent branch label, (|0> + |1>) / sqrt(2).
extended_swap_test.h(ancilla)

# Apply U(t) only to the |1> branch of the ancilla.
extended_swap_test.append(
    controlled_evolution_gate, [ancilla] + system_qubits
)

# Decompose once more for visualization so that control bullets are visible.
display(extended_swap_test.draw("mpl", fold=-1))

Output:

Output of the previous code cell

オブザーバブル

任意のエルミート系における観測量 OO について、ここでは計算を行うために観測量を次のように設定する:

z=⟨ψ0∣O∣ψd⟩.\begin{equation*} z=\langle \psi_0|O|\psi_d\rangle . \end{equation*}

これは、 O=IO=I が重なり成分 S0d=⟨ψ0∣ψd⟩\mathcal{S}_{0d}=\langle\psi_0|\psi_d\rangle を与えるのに対し、 O=HO=H はハミルトニアン成分 H0d=⟨ψ0∣H∣ψd⟩\mathcal{H}_{0d}=\langle\psi_0|H|\psi_d\rangle を与えるためである。

X=∣0⟩⟨1∣+∣1⟩⟨0∣X=|0\rangle\langle 1|+|1\rangle\langle 0| を用いると、次のようになります

⟨Ψ0d∣X⊗O∣Ψ0d⟩=(⟨0∣⟨ψ0∣+⟨1∣⟨ψd∣2)(X⊗O)(∣0⟩∣ψ0⟩+∣1⟩∣ψd⟩2)=12(⟨ψ0∣O∣ψd⟩+⟨ψd∣O∣ψ0⟩)=z+z∗2=Re⁡z.\begin{align*} \langle \Psi_{0d}|X\otimes O| \Psi_{0d} \rangle &= \left(\frac{\langle0|\langle\psi_0| + \langle1| \langle\psi_d|}{\sqrt{2}}\right)(X\otimes O)\left(\frac{|0\rangle|\psi_0\rangle + |1\rangle |\psi_d\rangle}{\sqrt{2}}\right) \\ &= \frac{1}{2} \left( \langle \psi_0|O|\psi_d\rangle + \langle \psi_d|O|\psi_0\rangle \right) \\ &= \frac{z+z^*}{2} = \operatorname{Re} z. \end{align*}

同様に、 Y=−i∣0⟩⟨1∣+i∣1⟩⟨0∣Y=-i|0\rangle\langle 1|+i|1\rangle\langle 0| を使用すると、

⟨Ψ0d∣Y⊗O∣Ψ0d⟩=(⟨0∣⟨ψ0∣+⟨1∣⟨ψd∣2)(Y⊗O)(∣0⟩∣ψ0⟩+∣1⟩∣ψd⟩2)=12(−i⟨ψ0∣O∣ψd⟩+i⟨ψd∣O∣ψ0⟩)=−iz+iz∗2=Im⁡z.\begin{align*} \langle \Psi_{0d}|Y\otimes O| \Psi_{0d} \rangle &= \left(\frac{\langle0|\langle\psi_0| + \langle1| \langle\psi_d|}{\sqrt{2}}\right)(Y\otimes O)\left(\frac{|0\rangle|\psi_0\rangle + |1\rangle |\psi_d\rangle}{\sqrt{2}}\right) \\ &= \frac{1}{2} \left( -i\langle \psi_0|O|\psi_d\rangle + i\langle \psi_d|O|\psi_0\rangle \right) \\ &= \frac{-iz+iz^*}{2} = \operatorname{Im} z. \end{align*}

したがって、次のようになります

⟨Φ0d∣X⊗O∣Φ0d⟩=Re⁡⟨ψ0∣O∣ψd⟩,⟨Φ0d∣Y⊗O∣Φ0d⟩=Im⁡⟨ψ0∣O∣ψd⟩.\begin{equation*} \langle \Phi_{0d}|X\otimes O|\Phi_{0d}\rangle = \operatorname{Re}\langle\psi_0|O|\psi_d\rangle , \quad \langle \Phi_{0d}|Y\otimes O|\Phi_{0d}\rangle = \operatorname{Im}\langle\psi_0|O|\psi_d\rangle . \end{equation*}

最後に、各状態 ∣Ψ0d⟩|\Psi_{0d}\rangle について、以下の値を測定する必要があります:

X⊗I for Re⁡S0d,Y⊗I for Im⁡S0d,X⊗H for Re⁡H0d,Y⊗H for Im⁡H0d.\begin{align*} X \otimes I \text{ for } \operatorname{Re}\mathcal{S}_{0d},\\ Y \otimes I \text{ for } \operatorname{Im}\mathcal{S}_{0d},\\ X \otimes H \text{ for } \operatorname{Re}\mathcal{H}_{0d},\\ Y \otimes H \text{ for } \operatorname{Im}\mathcal{H}_{0d}.\\ \end{align*}
n_qubits = hamiltonian.num_qubits

observable_labels = [
    "Re S_0d",
    "Im S_0d",
    "Re H_0d",
    "Im H_0d",
]

# X ⊗ I and Y ⊗ I.
# Qiskit's Pauli-label convention places qubit 0 on the rightmost character,
# so the ancilla Pauli is appended to the right.
obs_x_identity = SparsePauliOp("I" * n_qubits + "X")
obs_y_identity = SparsePauliOp("I" * n_qubits + "Y")

# X ⊗ H and Y ⊗ H.
obs_x_hamiltonian = SparsePauliOp.from_list(
    [
        (label + "X", coeff)
        for label, coeff in zip(
            hamiltonian.paulis.to_labels(),
            hamiltonian.coeffs,
        )
    ]
)

obs_y_hamiltonian = SparsePauliOp.from_list(
    [
        (label + "Y", coeff)
        for label, coeff in zip(
            hamiltonian.paulis.to_labels(),
            hamiltonian.coeffs,
        )
    ]
)

observables = [
    obs_x_identity,
    obs_y_identity,
    obs_x_hamiltonian,
    obs_y_hamiltonian,
]

for obs, label in zip(observables, observable_labels):
    print(f"Observable: {label}")
    print(obs)
    print()

Output:

Observable: Re S_0d
SparsePauliOp(['IIIIIIIIIIIIX'],
              coeffs=[1.+0.j])

Observable: Im S_0d
SparsePauliOp(['IIIIIIIIIIIIY'],
              coeffs=[1.+0.j])

Observable: Re H_0d
SparsePauliOp(['IIIIIIIIIIXXX', 'IIIIIIIIIXXIX', 'IIIIIIIIXXIIX', 'IIIIIIIXXIIIX', 'IIIIIIXXIIIIX', 'IIIIIXXIIIIIX', 'IIIIXXIIIIIIX', 'IIIXXIIIIIIIX', 'IIXXIIIIIIIIX', 'IXXIIIIIIIIIX', 'XXIIIIIIIIIIX', 'IIIIIIIIIIYYX', 'IIIIIIIIIYYIX', 'IIIIIIIIYYIIX', 'IIIIIIIYYIIIX', 'IIIIIIYYIIIIX', 'IIIIIYYIIIIIX', 'IIIIYYIIIIIIX', 'IIIYYIIIIIIIX', 'IIYYIIIIIIIIX', 'IYYIIIIIIIIIX', 'YYIIIIIIIIIIX', 'IIIIIIIIIIZZX', 'IIIIIIIIIZZIX', 'IIIIIIIIZZIIX', 'IIIIIIIZZIIIX', 'IIIIIIZZIIIIX', 'IIIIIZZIIIIIX', 'IIIIZZIIIIIIX', 'IIIZZIIIIIIIX', 'IIZZIIIIIIIIX', 'IZZIIIIIIIIIX', 'ZZIIIIIIIIIIX'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

Observable: Im H_0d
SparsePauliOp(['IIIIIIIIIIXXY', 'IIIIIIIIIXXIY', 'IIIIIIIIXXIIY', 'IIIIIIIXXIIIY', 'IIIIIIXXIIIIY', 'IIIIIXXIIIIIY', 'IIIIXXIIIIIIY', 'IIIXXIIIIIIIY', 'IIXXIIIIIIIIY', 'IXXIIIIIIIIIY', 'XXIIIIIIIIIIY', 'IIIIIIIIIIYYY', 'IIIIIIIIIYYIY', 'IIIIIIIIYYIIY', 'IIIIIIIYYIIIY', 'IIIIIIYYIIIIY', 'IIIIIYYIIIIIY', 'IIIIYYIIIIIIY', 'IIIYYIIIIIIIY', 'IIYYIIIIIIIIY', 'IYYIIIIIIIIIY', 'YYIIIIIIIIIIY', 'IIIIIIIIIIZZY', 'IIIIIIIIIZZIY', 'IIIIIIIIZZIIY', 'IIIIIIIZZIIIY', 'IIIIIIZZIIIIY', 'IIIIIZZIIIIIY', 'IIIIZZIIIIIIY', 'IIIZZIIIIIIIY', 'IIZZIIIIIIIIY', 'IZZIIIIIIIIIY', 'ZZIIIIIIIIIIY'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

H\mathcal{H} 行列要素の推定においては、 S\mathcal{S} 行列要素の場合に比べて、パウリ項の数がはるかに多い。

ここで、シフト法 [4] を用いることで、測定されるハミルトニアン項の数を減らすことができる。 ハミルトニアンを次のように分解する:

H=(H−T)+T,\begin{equation*} H = (H-T) + T, \end{equation*}

ここで、 TT は、基準状態がその固有状態となるように選ばれる

T∣ψ0⟩=τ∣ψ0⟩.\begin{equation*} T|\psi_0\rangle = \tau |\psi_0\rangle . \end{equation*}

それから、

H0d=⟨ψ0∣H∣ψd⟩=⟨ψ0∣(H−T)∣ψd⟩+⟨ψ0∣T∣ψd⟩=⟨ψ0∣(H−T)∣ψd⟩+τ⟨ψ0∣ψd⟩=H~0d+τS0d.\begin{align*} \mathcal{H}_{0d} &= \langle\psi_0|H|\psi_d\rangle \\ &= \langle\psi_0|(H-T)|\psi_d\rangle + \langle\psi_0|T|\psi_d\rangle \\ &= \langle\psi_0|(H-T)|\psi_d\rangle + \tau \langle\psi_0|\psi_d\rangle \\ &= \widetilde{\mathcal{H}}_{0d} + \tau \mathcal{S}_{0d}. \end{align*}

次のように編集します。

H~0d=⟨ψ0∣(H−T)∣ψd⟩\begin{equation*} \widetilde{\mathcal{H}}_{0d} = \langle\psi_0|(H-T)|\psi_d\rangle \end{equation*}

は、シフトされたハミルトニアン行列要素である。 したがって、測定する必要があるのは、 X⊗(H−T)X\otimes (H-T) および Y⊗(H−T)Y\otimes (H-T) のみである。 TT からの寄与は、すでに測定済みのオーバーラップ行列要素 S0d\mathcal{S}_{0d} を用いて、古典的に再構成される。

この例では、ハイゼンベルク・ハミルトニアンの対角成分が自然な選択肢となります

T=∑i=0n−2ZiZi+1.\begin{equation*} T = \sum_{i=0}^{n-2} Z_iZ_{i+1}. \end{equation*}

基準状態は計算基底状態であるため、すべての ZiZi+1Z_iZ_{i+1} 項の固有状態となる。

しかし、より有利な選択は、対角成分の ZZZZ 項だけでなく、基準状態を消滅させる XX+YYXX+YY 項も含めることである。

任意の隣接する2つの要素について、演算子 XX+YYXX+YY は次の条件を満たす

(XX+YY)∣00⟩=0,(XX+YY)∣11⟩=0,\begin{equation*} (XX+YY)|00\rangle = 0, \qquad (XX+YY)|11\rangle = 0, \end{equation*}

および

(XX+YY)∣01⟩=2∣10⟩,(XX+YY)∣10⟩=2∣01⟩.\begin{equation*} (XX+YY)|01\rangle = 2|10\rangle, \qquad (XX+YY)|10\rangle = 2|01\rangle . \end{equation*}

したがって、 XX+YYXX+YY という項が寄与するのは、隣接する2つの量子ビットが参照ビット列において異なる占有状態にある場合に限られる。 2つの量子ビットが両方とも 00 であるか、あるいは両方とも 11 である場合、この項は参照状態を消滅させるため、同様に除外することができる。

∣ψref⟩=∣z0⋯zn−1⟩|\psi_{\rm ref}\rangle = |z_0\cdots z_{n-1}\rangle と置くと、 zi∈{0,1}z_i \in \{0,1\} となる。 したがって、次のように選ぶことができる。

T=∑i=0n−2ZiZi+1+∑zi=zi+1(XiXi+1+YiYi+1).\begin{equation*} T=\sum_{i=0}^{n-2} Z_iZ_{i+1} + \sum_{z_i={z_{i+1}}} \left( X_iX_{i+1} + Y_iY_{i+1} \right). \end{equation*}

この演算子は依然として以下の条件を満たしています

T∣ψ0⟩=τ∣ψ0⟩,\begin{equation*} T|\psi_0\rangle = \tau |\psi_0\rangle , \end{equation*}

なぜなら、 ZZZZ の項は ∣ψ0⟩|\psi_0\rangle に対して対角線上に作用するのに対し、ずらされた XX+YYXX+YY の項はゼロになるからである。 したがって、対応する固有値は、 ZZZZ 項によってのみ決定される

τ=∑i=0n−2(−1)zi(−1)zi+1.\begin{equation*} \tau = \sum_{i=0}^{n-2} (-1)^{z_i} (-1)^{z_{i+1}} . \end{equation*}

この選択により、シフトされたハミルトニアンは次のようになる:

H−T=∑zi≠zi+1(XiXi+1+YiYi+1).\begin{equation*} H-T = \sum_{z_i\ne z_{i+1}} \left( X_iX_{i+1} + Y_iY_{i+1} \right). \end{equation*}

その結果、参照状態において職業が異なるエッジのみを測定すればよい。 すべての ZZZZ 項およびすべての非活性 XX+YYXX+YY 項は、オーバーラップ寄与 τS0d\tau \mathcal{S}_{0d} を通じて再構成されるか、あるいは定義上、寄与はゼロとなる。

これにより、対角部分のみをシフトする場合よりも、観測可能な範囲が小さくなります。 特に、局在した励起を持つ計算基底の参照状態の場合、 H−TH-T には、その励起に隣接するエッジのみが残る。 したがって、 X⊗(H−T)X\otimes(H-T) および Y⊗(H−T)Y\otimes(H-T) におけるパウリ項の数は大幅に削減できる一方で、再構成された行列要素は

H~0d+τS0d\begin{equation*} \widetilde{\mathcal{H}}_{0d} + \tau \mathcal{S}_{0d} \end{equation*}

まったく変わらない。

def make_reduced_heisenberg_observables(
    ref_bitstring: str,
    coupling: float = 1.0,
) -> tuple[SparsePauliOp, SparsePauliOp, float]:
    """Build X⊗(H-T), Y⊗(H-T), and tau."""
    n_qubits = len(ref_bitstring)

    shifted_terms: list[tuple[str, complex]] = []
    tau = 0.0

    def bit(q: int) -> str:
        return ref_bitstring[n_qubits - 1 - q]

    def append_term(q0: int, q1: int, pauli: str):
        label = ["I"] * n_qubits
        label[n_qubits - 1 - q0] = pauli[0]
        label[n_qubits - 1 - q1] = pauli[1]
        shifted_terms.append(("".join(label), coupling))

    for q in range(n_qubits - 1):
        same_occupation = bit(q) == bit(q + 1)

        # ZZ contribution to tau
        tau += coupling * (1.0 if same_occupation else -1.0)

        # XX + YY survives only for opposite occupations.
        if not same_occupation:
            append_term(q, q + 1, "XX")
            append_term(q, q + 1, "YY")

    if shifted_terms:
        obs_x_shifted_hamiltonian = SparsePauliOp.from_list(
            [(label + "X", coeff) for label, coeff in shifted_terms]
        )
        obs_y_shifted_hamiltonian = SparsePauliOp.from_list(
            [(label + "Y", coeff) for label, coeff in shifted_terms]
        )
    else:
        obs_x_shifted_hamiltonian = SparsePauliOp(
            "I" * n_qubits + "X", coeffs=[0.0]
        )
        obs_y_shifted_hamiltonian = SparsePauliOp(
            "I" * n_qubits + "Y", coeffs=[0.0]
        )

    return obs_x_shifted_hamiltonian, obs_y_shifted_hamiltonian, tau


obs_x_shifted_hamiltonian, obs_y_shifted_hamiltonian, shift_tau = (
    make_reduced_heisenberg_observables(ref_bitstring)
)

print("Observable: Re shifted H_0d")
print(obs_x_shifted_hamiltonian)
print()

print("Observable: Im shifted H_0d")
print(obs_y_shifted_hamiltonian)
print()

print("tau =", shift_tau)

observables = [
    obs_x_identity,
    obs_y_identity,
    obs_x_shifted_hamiltonian,
    obs_y_shifted_hamiltonian,
]
observable_labels = [
    "Re S_0d",
    "Im S_0d",
    "Re shifted H_0d",
    "Im shifted H_0d",
]

Output:

Observable: Re shifted H_0d
SparsePauliOp(['IIIIIXXIIIIIX', 'IIIIIYYIIIIIX', 'IIIIXXIIIIIIX', 'IIIIYYIIIIIIX'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

Observable: Im shifted H_0d
SparsePauliOp(['IIIIIXXIIIIIY', 'IIIIIYYIIIIIY', 'IIIIXXIIIIIIY', 'IIIIYYIIIIIIY'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

tau = 7.0

ステップ2:量子ハードウェア実行に向けた問題の最適化

ここで、抽象的な拡張スワップテスト回路を、ハードウェア向けのテンプレートに変換します。 その前に、まず抽象的なレベルで回路をさらに最適化します。

ハミルトニアン項の順序を比較する

まず、ハミルトニアンシミュレーションのために、ハイゼンベルク・ハミルトニアンにおけるパウリ項のさまざまな順序を比較する。 ハミルトニアン自体は変わらないが、順序付けによって、積の公式を用いた回路の生成方法や、回路内で構造をどの程度並列化できるかが左右される。 たとえば、単純な順序付けでは、まずすべての最近傍 XXXX 項、次にすべての YYYY 項、そしてすべての ZZZZ 項がリストされます。 これにより、 (0,1)(0,1) や (1,2)(1,2) といった隣接するエッジが互いに隣り合う配置となるため、これらは並列に実行できなくなります。 「偶数→奇数」の順序では、まず交わらない偶数エッジを順に処理し、続いて奇数エッジを処理することで、並列な2クビット層が明らかになります。 「偶数・奇数の辺グループ分け順序付け」は、さらに一歩進んだものです。各辺について、 XXXX、 YYYY、 ZZZZ という項をまとめて扱いながら、偶数の辺を奇数の辺よりも先に処理します。 「偶数-奇数」および「偶数-奇数」のエッジグループ化順序により、並列な2量子ビット層が露呈されることで回路深度が低減されると予想されるほか、エッジグループ化順序では、同一エッジ上の局所的な2量子ビット相互作用がコンパクトなブロックとして扱われるため、トロッター誤差がさらに低減されると考えられる。

項の順序が異なるパウリ演化

ハミルトニアン順もトロッター誤差に影響を及ぼします。 非可換な項を隣り合わせに配置すると、基底遷移がより頻繁に発生し、その結果、トロッター誤差が大きくなる。 同じパウリ基底変換を必要とする項をグループ化することで、冗長な基底変換を避けることができる。

ここでは、同じ転置条件を用いて、第1行のクリロフ推定値に現れる最大の進化時間 tmax⁡=(r−1)Δtt_{\max} = (r-1)\Delta t を用いて比較を行っています。

トロッター誤差を測定するために、トロッター化された回路と厳密なハミルトニアン進化との間の過程の不整合を用いる

Infidelity(U,V)=1−F(U,V)=1−∣Tr⁡(U†V)∣2d2,\begin{equation*} \text{Infidelity}(U,V) = 1-F(U,V) = 1 - \frac{|\operatorname{Tr}(U^\dagger V)|^2}{d^2}, \end{equation*}

ここで、 d=2nd=2^n はヒルベルト空間の次元である。

この診断処理は高密度行列を使用するため、この12キュービットという小規模な例には適していますが、スケーラブルなサブルーチンとして設計されたものではありません。

# The comparison uses the largest time that appears in the first-row Krylov estimates.
# Circuit depth does not depend on this numeric value, but the Trotter error does.
comparison_time = (krylov_dim - 1) * dt


def make_heisenberg_hamiltonian_ordered(
    num_qubits: int,
    ordering: str,
    coupling: float = 1.0,
) -> SparsePauliOp:
    """Return the same Heisenberg Hamiltonian with a specified term ordering."""
    terms: list[tuple[str, complex]] = []
    even_edges = [(q, q + 1) for q in range(0, num_qubits - 1, 2)]
    odd_edges = [(q, q + 1) for q in range(1, num_qubits - 1, 2)]

    def append_term(q0: int, q1: int, pauli: str):
        label = ["I"] * num_qubits
        label[num_qubits - 1 - q0] = pauli[0]
        label[num_qubits - 1 - q1] = pauli[1]
        terms.append(("".join(label), coupling))

    if ordering == "naive":
        for pauli in ("XX", "YY", "ZZ"):
            for q in range(num_qubits - 1):
                append_term(q, q + 1, pauli)
    elif ordering == "even-then-odd":
        for pauli in ("XX", "YY", "ZZ"):
            for q0, q1 in even_edges + odd_edges:
                append_term(q0, q1, pauli)
    elif ordering == "even-odd edge-grouped":
        for q0, q1 in even_edges + odd_edges:
            for pauli in ("XX", "YY", "ZZ"):
                append_term(q0, q1, pauli)
    else:
        raise ValueError(f"Unknown ordering: {ordering}")

    return SparsePauliOp.from_list(terms).simplify()


def build_numeric_evolution_circuit(
    hamiltonian: SparsePauliOp,
    synthesis,
    time_value: float,
    **synthesis_kwargs,
) -> QuantumCircuit:
    """Build a numeric circuit for exp(-i H t) with a chosen synthesis rule."""
    evolution_gate = PauliEvolutionGate(
        hamiltonian,
        time=time_value,
        synthesis=synthesis(**synthesis_kwargs),
    )
    circuit = QuantumCircuit(hamiltonian.num_qubits)
    circuit.append(evolution_gate, range(hamiltonian.num_qubits))
    return circuit


def process_infidelity(
    circuit: QuantumCircuit,
    exact_matrix: np.ndarray,
) -> float:
    """Return 1 - |Tr(U_circuit† U_exact) / d|²."""
    circuit_matrix = np.asarray(Operator(circuit).data)
    dim = circuit_matrix.shape[0]
    normalized_trace = np.vdot(circuit_matrix, exact_matrix) / dim
    fidelity = np.abs(normalized_trace) ** 2
    return float(np.clip(1.0 - fidelity, 0.0, 1.0))


hamiltonians_by_ordering = {
    ordering: make_heisenberg_hamiltonian_ordered(
        num_qubits, ordering, coupling=1.0
    )
    for ordering in ["naive", "even-then-odd", "even-odd edge-grouped"]
}

print(f"Comparison time: {comparison_time}\n")

for order_name, ham_ordered in hamiltonians_by_ordering.items():
    print(f"{order_name}:")
    print([op for op, _ in ham_ordered.to_list()])
    print()

print("Precomputing the exact evolution operator... ", end="")
exact_matrix = la.expm(-1j * comparison_time * hamiltonian.to_matrix())
print("Done")

Output:

Comparison time: 0.8567979964335799

naive:
['IIIIIIIIIIXX', 'IIIIIIIIIXXI', 'IIIIIIIIXXII', 'IIIIIIIXXIII', 'IIIIIIXXIIII', 'IIIIIXXIIIII', 'IIIIXXIIIIII', 'IIIXXIIIIIII', 'IIXXIIIIIIII', 'IXXIIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIIYYI', 'IIIIIIIIYYII', 'IIIIIIIYYIII', 'IIIIIIYYIIII', 'IIIIIYYIIIII', 'IIIIYYIIIIII', 'IIIYYIIIIIII', 'IIYYIIIIIIII', 'IYYIIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIIZZI', 'IIIIIIIIZZII', 'IIIIIIIZZIII', 'IIIIIIZZIIII', 'IIIIIZZIIIII', 'IIIIZZIIIIII', 'IIIZZIIIIIII', 'IIZZIIIIIIII', 'IZZIIIIIIIII', 'ZZIIIIIIIIII']

even-then-odd:
['IIIIIIIIIIXX', 'IIIIIIIIXXII', 'IIIIIIXXIIII', 'IIIIXXIIIIII', 'IIXXIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIXXIII', 'IIIIIXXIIIII', 'IIIXXIIIIIII', 'IXXIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIYYII', 'IIIIIIYYIIII', 'IIIIYYIIIIII', 'IIYYIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIYYI', 'IIIIIIIYYIII', 'IIIIIYYIIIII', 'IIIYYIIIIIII', 'IYYIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIZZII', 'IIIIIIZZIIII', 'IIIIZZIIIIII', 'IIZZIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIZZI', 'IIIIIIIZZIII', 'IIIIIZZIIIII', 'IIIZZIIIIIII', 'IZZIIIIIIIII']

even-odd edge-grouped:
['IIIIIIIIIIXX', 'IIIIIIIIIIYY', 'IIIIIIIIIIZZ', 'IIIIIIIIXXII', 'IIIIIIIIYYII', 'IIIIIIIIZZII', 'IIIIIIXXIIII', 'IIIIIIYYIIII', 'IIIIIIZZIIII', 'IIIIXXIIIIII', 'IIIIYYIIIIII', 'IIIIZZIIIIII', 'IIXXIIIIIIII', 'IIYYIIIIIIII', 'IIZZIIIIIIII', 'XXIIIIIIIIII', 'YYIIIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIIIYYI', 'IIIIIIIIIZZI', 'IIIIIIIXXIII', 'IIIIIIIYYIII', 'IIIIIIIZZIII', 'IIIIIXXIIIII', 'IIIIIYYIIIII', 'IIIIIZZIIIII', 'IIIXXIIIIIII', 'IIIYYIIIIIII', 'IIIZZIIIIIII', 'IXXIIIIIIIII', 'IYYIIIIIIIII', 'IZZIIIIIIIII']

Precomputing the exact evolution operator... Done

まず、合成則を1次のトロッターステップに固定し、パウリ項の順序のみを変化させてみる。 この比較の目的は、主に、トランスパイラーに対して互いに交わらない最近傍エッジを露出させることで、回路の深さと2量子ビットあたりのコストをどの程度削減できるかを確認することにあります。

ordering_comparison_rows = []
infidelity_reps = [1, 2, 4, 8]

for ordering, ham_ordered in hamiltonians_by_ordering.items():
    for reps in infidelity_reps:
        circuit = build_numeric_evolution_circuit(
            ham_ordered,
            LieTrotter,
            comparison_time,
            reps=reps,
        )
        decomposed_circuit = simple_transpilation(circuit)
        infidelity = process_infidelity(decomposed_circuit, exact_matrix)
        ordering_comparison_rows.append(
            {
                "ordering": ordering,
                "synthesis": f"LieTrotter(reps={reps})",
                "infidelity": infidelity,
                **summarize_circuit(decomposed_circuit),
            }
        )
        if reps == 1:
            print(f"Circuit for {ordering} ordering:")
            print([op for op, _ in ham_ordered.to_list()])
            display(decomposed_circuit.draw("mpl", fold=-1, scale=0.6))

ordering_comparison_df = pd.DataFrame(ordering_comparison_rows)
display(ordering_comparison_df)

hamiltonian_for_synthesis = hamiltonians_by_ordering["even-odd edge-grouped"]

Output:

Circuit for naive ordering:
['IIIIIIIIIIXX', 'IIIIIIIIIXXI', 'IIIIIIIIXXII', 'IIIIIIIXXIII', 'IIIIIIXXIIII', 'IIIIIXXIIIII', 'IIIIXXIIIIII', 'IIIXXIIIIIII', 'IIXXIIIIIIII', 'IXXIIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIIYYI', 'IIIIIIIIYYII', 'IIIIIIIYYIII', 'IIIIIIYYIIII', 'IIIIIYYIIIII', 'IIIIYYIIIIII', 'IIIYYIIIIIII', 'IIYYIIIIIIII', 'IYYIIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIIZZI', 'IIIIIIIIZZII', 'IIIIIIIZZIII', 'IIIIIIZZIIII', 'IIIIIZZIIIII', 'IIIIZZIIIIII', 'IIIZZIIIIIII', 'IIZZIIIIIIII', 'IZZIIIIIIIII', 'ZZIIIIIIIIII']
Output of the previous code cell
Circuit for even-then-odd ordering:
['IIIIIIIIIIXX', 'IIIIIIIIXXII', 'IIIIIIXXIIII', 'IIIIXXIIIIII', 'IIXXIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIXXIII', 'IIIIIXXIIIII', 'IIIXXIIIIIII', 'IXXIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIYYII', 'IIIIIIYYIIII', 'IIIIYYIIIIII', 'IIYYIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIYYI', 'IIIIIIIYYIII', 'IIIIIYYIIIII', 'IIIYYIIIIIII', 'IYYIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIZZII', 'IIIIIIZZIIII', 'IIIIZZIIIIII', 'IIZZIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIZZI', 'IIIIIIIZZIII', 'IIIIIZZIIIII', 'IIIZZIIIIIII', 'IZZIIIIIIIII']
Output of the previous code cell
Circuit for even-odd edge-grouped ordering:
['IIIIIIIIIIXX', 'IIIIIIIIIIYY', 'IIIIIIIIIIZZ', 'IIIIIIIIXXII', 'IIIIIIIIYYII', 'IIIIIIIIZZII', 'IIIIIIXXIIII', 'IIIIIIYYIIII', 'IIIIIIZZIIII', 'IIIIXXIIIIII', 'IIIIYYIIIIII', 'IIIIZZIIIIII', 'IIXXIIIIIIII', 'IIYYIIIIIIII', 'IIZZIIIIIIII', 'XXIIIIIIIIII', 'YYIIIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIIIYYI', 'IIIIIIIIIZZI', 'IIIIIIIXXIII', 'IIIIIIIYYIII', 'IIIIIIIZZIII', 'IIIIIXXIIIII', 'IIIIIYYIIIII', 'IIIIIZZIIIII', 'IIIXXIIIIIII', 'IIIYYIIIIIII', 'IIIZZIIIIIII', 'IXXIIIIIIIII', 'IYYIIIIIIIII', 'IZZIIIIIIIII']
Output of the previous code cell
, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,0naiveLieTrotter(reps=1)0.999917153333151naiveLieTrotter(reps=2)0.805998216666212naiveLieTrotter(reps=4)0.27138933132132333naiveLieTrotter(reps=8)0.07459057264264574even-then-oddLieTrotter(reps=1)0.9999176333365even-then-oddLieTrotter(reps=2)0.805998126666126even-then-oddLieTrotter(reps=4)0.27138924132132247even-then-oddLieTrotter(reps=8)0.07459048264264488even-odd edge-groupedLieTrotter(reps=1)0.9984326333369even-odd edge-groupedLieTrotter(reps=2)0.6538431266661210even-odd edge-groupedLieTrotter(reps=4)0.181529241321322411even-odd edge-groupedLieTrotter(reps=8)0.0452444826426448 ,

において reps=1、偶数-奇数および偶数-奇数のエッジグループ化順序では、いずれも並列な2量子ビット層を露出させることで深さが15から6へと減少するが、3つの順序すべてにおいて2量子ビットゲートの数は変わらないことが確認された。 しかし、これら3つの順序付けはいずれも不忠実度が1に近い値をとるため、順序付けをより明確に区別するために、トロッター反復の回数を変化させていく。 反復回数が増えるにつれて、順序 even-odd edge-grouped 付けの不正確さは他の2つよりも急速に低下し、 0.045 に達するのに対し reps=8 、単純な順序付けおよび「偶数→奇数」の順序付けでは 0.075 にとどまる。

製品と化学式の合成を比較する

次に、ハミルトニアンを「偶数・奇数の辺グループ化」順序に固定した状態で、トロッター化のさまざまな高度な設定について検討します。 ここでは、1次のリー・トロッター、2次の鈴木・トロッター、および4次の鈴木・トロッターについて考察する。

synthesis_comparison_rows = []

for num_trotter_steps in [1, 2, 3, 4, 5]:
    synthesis_cases = [
        ("LieTrotter", LieTrotter, {"reps": num_trotter_steps}),
        (
            "SuzukiTrotter(order=2)",
            SuzukiTrotter,
            {"order": 2, "reps": num_trotter_steps},
        ),
        (
            "SuzukiTrotter(order=4)",
            SuzukiTrotter,
            {"order": 4, "reps": num_trotter_steps},
        ),
    ]
    for label, synthesis, kwargs in synthesis_cases:
        circuit = build_numeric_evolution_circuit(
            hamiltonian_for_synthesis,
            synthesis,
            comparison_time,
            **kwargs,
        )
        decomposed_circuit = simple_transpilation(circuit)
        synthesis_comparison_rows.append(
            {
                "synthesis": label,
                "reps": kwargs["reps"],
                "infidelity": process_infidelity(
                    decomposed_circuit, exact_matrix
                ),
                **summarize_circuit(decomposed_circuit),
            }
        )

synthesis_comparison_df = pd.DataFrame(synthesis_comparison_rows)
display(synthesis_comparison_df.sort_values(["2q gates", "2q depth"]))

# For memory free
exact_matrix = None

Output:

, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,0LieTrotter19.984324e-016333361SuzukiTrotter(order=2)19.733399e-019515193LieTrotter26.538427e-01126666124SuzukiTrotter(order=2)22.522533e-01158484156LieTrotter33.197242e-01189999187SuzukiTrotter(order=2)34.804050e-0221117117219LieTrotter41.815291e-01241321322410SuzukiTrotter(order=2)41.453103e-02271501502712LieTrotter51.161770e-0130165165302SuzukiTrotter(order=4)12.884402e-01331831833313SuzukiTrotter(order=2)55.803455e-0333183183335SuzukiTrotter(order=4)21.641162e-0363348348638SuzukiTrotter(order=4)32.907076e-05935135139311SuzukiTrotter(order=4)42.791061e-0612367867812314SuzukiTrotter(order=4)54.736685e-07153843843153 ,

Lie-Trotter法は回路の深さが最も浅いものの、誤差が最も大きくなるのに対し、4次のSuzuki-Trotter法は精度が高い反面、回路の深さが増加する。 このチュートリアルの残りの部分では、1次式に比べてトロッター誤差を大幅に低減しつつ、深さの小さい回路が得られるため、2次スズキ・トロッター法を採用する。

制御された時間発展ゲートを削除する

拡張スワップテストにおいて、単一のアンシラ量子ビットを用いて時間的変化を制御するには、そのアンシラがシステム全体にわたって多数のゲートを制御する必要がある。 これにより、ルーティングのオーバーヘッドが大幅に増大する可能性があり、最悪の場合、事実上「オール・トゥ・ワン」の接続が求められることになる。 これを回避するために、ハミルトニアンの対称性を活用し、制御付き時間発展ゲートを制御不要なバージョンに置き換えることで、さらなる最適化が可能となる。 次の回路を見てみましょう。

回路の最適化

ここでは、 BrefB_{\rm ref} が参照状態 Bref∣0n⟩=∣ψ0⟩B_{\rm ref}|0^n\rangle = |\psi_{0}\rangle を生成します。

最初に 12(∣0⟩+∣1⟩)∣ψ0⟩\frac{1}{\sqrt{2}}(|0\rangle+|1\rangle)|\psi_0\rangle を準備し、その後 ∣1⟩|1\rangle ブランチに対してのみ U(t)U(t) を適用する代わりに、この回路では以下のとおり、2つのブランチを直接準備します

∣0⟩∣ψ0⟩and∣1⟩∣ψd⟩,\begin{equation*} |0\rangle|\psi_0\rangle \quad \text{and} \quad |1\rangle|\psi_d\rangle , \end{equation*}

各項の説明は次のとおりです。

∣ψd⟩=U(dΔt)∣ψ0⟩.\begin{equation*} |\psi_d\rangle = U(d\Delta t)|\psi_0\rangle . \end{equation*}

この回路はまず、アンシラにアダマールゲートを適用し、 ∣1⟩|1\rangle の分岐上でのみ参照状態を準備する:

∣0⟩∣0n⟩⟶∣0⟩∣0n⟩+∣1⟩Bref∣0n⟩2=∣0⟩∣0n⟩+∣1⟩∣ψ0⟩2.\begin{equation*} |0\rangle |0^n\rangle \longrightarrow \frac{|0\rangle |0^n\rangle + |1\rangle B_{\rm ref}|0^n\rangle}{\sqrt{2}} = \frac{|0\rangle |0^n\rangle + |1\rangle |\psi_0\rangle}{\sqrt{2}} . \end{equation*}

次に、制御されていない時間発展演算子を両方の分岐に適用する:

∣0⟩∣0n⟩+∣1⟩∣ψ0⟩2⟶∣0⟩U(t)∣0n⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{|0\rangle |0^n\rangle + |1\rangle |\psi_0\rangle}{\sqrt{2}} \longrightarrow \frac{|0\rangle U(t)|0^n\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

ハミルトニアンは励起数を保存するため、 ∣0n⟩|0^n\rangle がその固有状態であることがわかる。したがって、 ハミルトニアンの下では、進化演算子は位相のみを蓄積する:

U(t)∣0n⟩=e−iEvact∣0n⟩.\begin{equation*} U(t)|0^n\rangle = e^{-iE_{\rm vac}t}|0^n\rangle . \end{equation*}

したがって、

∣0⟩U(t)∣0n⟩+∣1⟩U(t)∣ψ0⟩2=e−iEvact∣0⟩∣0n⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{|0\rangle U(t)|0^n\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} = \frac{e^{-iE_{\rm vac}t}|0\rangle |0^n\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

次に、 BrefB_{\rm ref} は ∣0⟩|0\rangle ブランチにのみ適用されます。

e−iEvact∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{e^{-iE_{\rm vac}t}|0\rangle |\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

この時点で、2つの分岐には余分な相対位相が生じている。 これを除去するために、アンシラ・フェーズゲートを適用します

P(−Evact)=(100e−iEvact).\begin{equation*} P(-E_{\rm vac}t) = \begin{pmatrix} 1 & 0 \\ 0 & e^{-iE_{\rm vac}t} \end{pmatrix}. \end{equation*}

これにより、状態は次のように変換されます

e−iEvact∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2⟶e−iEvact∣0⟩∣ψ0⟩+e−iEvact∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{e^{-iE_{\rm vac}t}|0\rangle |\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} \longrightarrow \frac{e^{-iE_{\rm vac}t}|0\rangle |\psi_0\rangle + e^{-iE_{\rm vac}t}|1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

したがって、無視できる程度のグローバル位相の範囲内において、我々はついに以下を調製した

∣Φ0d⟩=∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} |\Phi_{0d}\rangle = \frac{ |0\rangle|\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle }{\sqrt{2}} . \end{equation*}

次のコードブロックでは、この制御不要回路を実装します。

controlled_extended_swap_test = extended_swap_test

# Reuse the ordering and synthesis rule selected by the comparison above.
evol_gate_optimized = PauliEvolutionGate(
    hamiltonian_for_synthesis,
    time=t,
    synthesis=SuzukiTrotter(order=2, reps=num_trotter_steps),
)

uncontrolled_evolution = QuantumCircuit(num_qubits, name="U_ST2(t)")
uncontrolled_evolution.append(evol_gate_optimized, range(num_qubits))

controlled_state_prep = QuantumCircuit(num_qubits + 1, name="C-Prep")
for q, bit in enumerate(reversed(ref_bitstring)):
    if bit == "1":
        controlled_state_prep.cx(ancilla, system_qubits[q])

vacuum_bitstring = "0" * num_qubits
vacuum_energy = basis_state_expectation_sparse(hamiltonian, "0" * num_qubits)

optimized_extended_swap_test = QuantumCircuit(num_qubits + 1)
optimized_extended_swap_test.h(ancilla)

# Prepare |psi_ref> only on the |1> branch.
optimized_extended_swap_test.compose(controlled_state_prep, inplace=True)
optimized_extended_swap_test.barrier()

# Apply the Trotterized time evolution without control.
optimized_extended_swap_test.compose(
    uncontrolled_evolution,
    qubits=system_qubits,
    inplace=True,
)
optimized_extended_swap_test.barrier()

# Map |0>|0...0> to |0>|psi_ref>, leaving the |1> branch unchanged.
optimized_extended_swap_test.x(ancilla)
optimized_extended_swap_test.compose(controlled_state_prep, inplace=True)
optimized_extended_swap_test.x(ancilla)

# Cancel the known vacuum phase so that the same X/Y observables can be used.
optimized_extended_swap_test.p(-vacuum_energy * t, ancilla)
optimized_extended_swap_test = simple_transpilation(
    optimized_extended_swap_test
)

print(f"Vacuum energy E_vac = {vacuum_energy:.1f}")
display(
    optimized_extended_swap_test.assign_parameters({t: 1.0}).draw(
        "mpl", scale=0.5, fold=26
    )
)

Output:

Vacuum energy E_vac = 11.0+0.0j
Output of the previous code cell

トランスパイレーション

さて、制御付き回路と制御なし回路を、ハードウェア上で実行可能になるようトランスパイルします。 トランスパイルされた回路の結果を比較してみましょう。

pass_manager = generate_preset_pass_manager(
    backend=backend,
    optimization_level=3,
)

isa_controlled_extended_swap_test = pass_manager.run(
    controlled_extended_swap_test
)

pass_manager = generate_preset_pass_manager(
    backend=backend, optimization_level=3, routing_method="none"
)
isa_optimized_extended_swap_test = pass_manager.run(
    optimized_extended_swap_test
)

transpilation_result = [
    {
        "label": "abstract controlled U(t)",
        **summarize_circuit(isa_controlled_extended_swap_test),
    },
    {
        "label": "optimized non-controlled U(t)",
        **summarize_circuit(isa_optimized_extended_swap_test),
    },
]

display(pd.DataFrame(transpilation_result))


def filter_qubits_from_layout(layout):
    q_layout = Layout(
        {
            physical: virtual
            for physical, virtual in layout.get_physical_bits().items()
            if virtual._register.name == "q"
        }
    )
    return q_layout


print(
    filter_qubits_from_layout(
        isa_optimized_extended_swap_test.layout.initial_layout
    )
)

isa_observables = [
    op.apply_layout(isa_optimized_extended_swap_test.layout)
    for op in observables
]

Output:

, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,0abstract controlled U(t)1545723786468645801optimized non-controlled U(t)261171630757 ,
Layout({
18: <Qubit register=(13, "q"), index=0>,
5: <Qubit register=(13, "q"), index=1>,
6: <Qubit register=(13, "q"), index=2>,
7: <Qubit register=(13, "q"), index=3>,
8: <Qubit register=(13, "q"), index=4>,
9: <Qubit register=(13, "q"), index=5>,
10: <Qubit register=(13, "q"), index=6>,
11: <Qubit register=(13, "q"), index=7>,
12: <Qubit register=(13, "q"), index=8>,
13: <Qubit register=(13, "q"), index=9>,
14: <Qubit register=(13, "q"), index=10>,
15: <Qubit register=(13, "q"), index=11>,
19: <Qubit register=(13, "q"), index=12>
})

ステップ3: Qiskit primitivesを使用して実行する

次のステップは、同じパラメータ化された回路を、 dd のいくつかの値について適用することです。各 dd について、4つの期待値を推定します。すなわち、 X⊗IX\otimes I、 Y⊗IY\otimes I、 X⊗(H−T)X\otimes (H-T)、および Y⊗(H−T)Y\otimes (H-T) です。これら4つの数値は、その後、複素数の第1行の要素である S0d\mathcal{S}_{0d} および H0d\mathcal{H}_{0d} に統合されます。

ここでは、 ∣ψ0⟩\lvert\psi_0\rangle が疎行列であるため、 S00=1\mathcal{S}_{00}=1 および H00=⟨ψ0∣(H−T)∣ψ0⟩=⟨ψ0∣H∣ψ0⟩−τ\mathcal{H}_{00}=\langle\psi_{0}|(H-T)|\psi_{0}\rangle=\langle\psi_{0}|H|\psi_{0}\rangle-\tau は従来の方法で計算できるため、 d=0d=0 の場合については省略する。

pub_list = []
d_values = list(range(1, krylov_dim))

# Exact local statevector estimator.
estimator = StatevectorEstimator()

# We use the circuit before the transpilation for the local simulator,
# but we will use the transpiled circuit for the real backend.
for d in d_values:
    parameter_values = [d * dt]

    for ob in observables:
        pub_list.append(
            (
                optimized_extended_swap_test,
                ob,
                parameter_values,
            )
        )

job = estimator.run(pub_list)

# Local PrimitiveJob does not provide Runtime-style job inputs,
# so preserve the inputs directly.
inputs = pub_list
result = job.result()

print(f"Number of Krylov basis states: r = {len(d_values)}")
print(f"Number of PUBs: {len(pub_list)}")
print(f"Each d uses observables: {observable_labels}")

Output:

Number of Krylov basis states: r = 9
Number of PUBs: 36
Each d uses observables: ['Re S_0d', 'Im S_0d', 'Re shifted H_0d', 'Im shifted H_0d']

ステップ4:後処理を行い、結果を希望の古典形式で返す

予測行列を推定した後、正則化を行い、GEVPを解く

Hc=ESc.\begin{equation*} \mathcal{H}\mathbf{c} = E\mathcal{S}\mathbf{c}. \end{equation*}

最小の一般化固有値から、基底状態のエネルギーに対するKQD推定値が得られる。

h_shifted_row_est = np.zeros(krylov_dim, dtype=complex)
s_row_est = np.zeros(krylov_dim, dtype=complex)

h_shifted_row_est[0] = ref_energy - shift_tau
s_row_est[0] = 1.0

for idx, (pub_input, pub_result) in enumerate(zip(inputs, result)):
    d_index, obs_index = divmod(idx, len(observables))
    ev = np.asarray(pub_result.data.evs).reshape(-1)[0]
    std = np.asarray(pub_result.data.stds).reshape(-1)[0]

    if obs_index == 0:
        s_row_est[d_index + 1] = ev
    elif obs_index == 1:
        s_row_est[d_index + 1] += 1j * ev
    elif obs_index == 2:
        h_shifted_row_est[d_index + 1] = ev
    elif obs_index == 3:
        h_shifted_row_est[d_index + 1] += 1j * ev

# H_0d = shifted_H_0d + tau * S_0d.
h_row_est = h_shifted_row_est + shift_tau * s_row_est

h_matrix_est = la.toeplitz(h_row_est.conj(), h_row_est)
s_matrix_est = la.toeplitz(s_row_est.conj(), s_row_est)

s_eigvals = la.eigvalsh(0.5 * (s_matrix_est + s_matrix_est.conj().T))
positive_s_eigvals = s_eigvals[s_eigvals > 1e-12]
s_condition_number = (
    positive_s_eigvals[-1] / positive_s_eigvals[0]
    if len(positive_s_eigvals) > 0
    else np.inf
)


with np.printoptions(precision=3, suppress=True):
    print("Estimated first row of S:")
    print(s_row_est)
    print()
    print("Estimated first row of H:")
    print(h_row_est)
    print()
    print("Eigenvalues of the estimated overlap matrix S:")
    print(s_eigvals)
    print(f"Condition number above 1e-12: {s_condition_number:.3e}")
    print()

Output:

Estimated first row of S:
[ 1.   +0.j     0.758-0.596j  0.203-0.836j -0.291-0.636j -0.444-0.229j
 -0.276+0.053j -0.044+0.051j  0.006-0.121j -0.155-0.218j -0.345-0.101j]

Estimated first row of H:
[ 7.   +0.j     4.842-4.76j   0.044-6.185j -3.791-3.653j -4.137+0.39j
 -1.495+2.654j  1.331+1.777j  1.85 -0.763j -0.032-2.278j -2.222-1.372j]

Eigenvalues of the estimated overlap matrix S:
[-0.     0.     0.     0.     0.     0.     0.01   0.355  3.526  6.109]
Condition number above 1e-12: 1.904e+12

ここで、回路推定値から再構成された行列を用いて、一般化固有値問題を解く。 正確なリアルタイムの時間発展を伴う理想的な状態ベクトルの計算においては、これにより正確な投影結果が再現されるはずです。 実際には、偏差はトロッター化、サンプリング誤差、およびオーバーラップ行列の数値的不安定性によって生じることがあります。

クリロフ部分空間の次元を増加させていくにつれて、エネルギーがどのように収束していくかを観察する。

exact_evals = diagonalize_single_1_subspace(hamiltonian)
exact_ground = min(exact_evals)
print("exact ground state energy:  ", exact_ground)

threshold = 1e-12
energy_convergence = []

for r in range(1, krylov_dim + 1):
    energy_est_kqd, coeffs_est, retained_est = solve_thresholded_gevp(
        h_matrix_est[:r, :r],
        s_matrix_est[:r, :r],
        threshold=threshold,
    )
    energy_convergence.append(energy_est_kqd)
    print(
        f"Krylov ground state energy (dim={r}, retained={retained_est}): ",
        energy_est_kqd,
    )

Output:

exact ground state energy:   3.136296694843727
Krylov ground state energy (dim=1, retained=1):  7.0
Krylov ground state energy (dim=2, retained=2):  4.184510657551266
Krylov ground state energy (dim=3, retained=3):  3.5539074630394136
Krylov ground state energy (dim=4, retained=4):  3.3366270761341044
Krylov ground state energy (dim=5, retained=5):  3.252017453225087
Krylov ground state energy (dim=6, retained=6):  3.2300275138879186
Krylov ground state energy (dim=7, retained=7):  3.2299154099085685
Krylov ground state energy (dim=8, retained=7):  3.2298063744216776
Krylov ground state energy (dim=9, retained=7):  3.2296778282872456
Krylov ground state energy (dim=10, retained=8):  3.223647515867734
def plot_energy_convergence(energy_convergence, exact_ground, krylov_dim):
    fig, ax = plt.subplots(figsize=(7, 4.5))

    ax.plot(
        range(1, krylov_dim + 1),
        energy_convergence,
        marker="o",
        label="KQD estimate",
    )

    ax.axhline(
        exact_ground,
        linestyle="--",
        label=f"Exact ground energy = {exact_ground:.6f}",
    )

    ax.set_xlabel("Krylov dimension")
    ax.set_ylabel("Ground-state energy")
    ax.grid(True, alpha=0.3)
    ax.legend()
    plt.tight_layout()
    plt.show()


plot_energy_convergence(energy_convergence, exact_ground, krylov_dim)

Output:

Output of the previous code cell

大規模なハードウェアの例

前の節では、状態ベクトルシミュレーションを診断ツールとして活用できるように、12キュービットのモデルを使用しました。 ここで、同じKQDワークフローを30キュービットのハイゼンベルグ鎖に拡張し、 IBM Quantum ハードウェア上で実行するための準備を行います。

手順 1~4 を 1 つのコードブロックにまとめました

ここでは、これらすべての詳細を統合して、より大規模な単一のワークフローを構築し、それを当社の実際の量子ハードウェア上で実行します。 このセクションでは、結果の信頼性を高めるために、現実的な誤差低減設定を適用する。 dd の異なる値に対応する行列要素は並列に計算できるため、これらを効率的に実行するために Batch modeを使用します。

# -------------------------Step 1-------------------------

# Map the classical problem to quantum circuits and observables.
# Problem and KQD parameters.
large_num_qubits = 30
large_krylov_dim = 7
large_num_trotter_steps = 3
large_dt = np.pi / (3 * (large_num_qubits - 1))
large_t = Parameter("t_large")

# Use a single excitation near the center of the chain.
large_excitation_qubit = large_num_qubits // 2
large_ref_label = ["0"] * large_num_qubits
large_ref_label[large_num_qubits - 1 - large_excitation_qubit] = "1"
large_ref_bitstring = "".join(large_ref_label)

# Use the ordering and product formula selected in the preceding section.
large_hamiltonian = make_heisenberg_hamiltonian_ordered(
    large_num_qubits,
    ordering="even-odd edge-grouped",
    coupling=1.0,
)
large_ref_energy = float(
    np.real(
        basis_state_expectation_sparse(large_hamiltonian, large_ref_bitstring)
    )
)
large_vacuum_energy = float(
    np.real(
        basis_state_expectation_sparse(
            large_hamiltonian, "0" * large_num_qubits
        )
    )
)

large_evolution_gate = PauliEvolutionGate(
    large_hamiltonian,
    time=large_t,
    synthesis=SuzukiTrotter(order=2, reps=large_num_trotter_steps),
)
large_uncontrolled_evolution = QuantumCircuit(
    large_num_qubits,
    name="U_ST2_large(t)",
)
large_uncontrolled_evolution.append(
    large_evolution_gate,
    range(large_num_qubits),
)

# Build the control-free extended-swap-test circuit.
large_ancilla = 0
large_system_qubits = list(range(1, large_num_qubits + 1))

large_controlled_state_prep = QuantumCircuit(
    large_num_qubits + 1,
    name="C-Prep-large",
)
large_controlled_state_prep.cx(
    large_ancilla,
    large_system_qubits[large_excitation_qubit],
)

large_extended_swap_test = QuantumCircuit(large_num_qubits + 1)
large_extended_swap_test.h(large_ancilla)
large_extended_swap_test.compose(large_controlled_state_prep, inplace=True)
large_extended_swap_test.compose(
    large_uncontrolled_evolution,
    qubits=large_system_qubits,
    inplace=True,
)
large_extended_swap_test.x(large_ancilla)
large_extended_swap_test.compose(
    large_controlled_state_prep.inverse(),
    inplace=True,
)
large_extended_swap_test.x(large_ancilla)
large_extended_swap_test.p(
    -large_vacuum_energy * large_t,
    large_ancilla,
)

# Reuse the Hamiltonian-shifting construction from the preceding section.
(
    large_obs_x_shifted_hamiltonian,
    large_obs_y_shifted_hamiltonian,
    large_shift_tau,
) = make_reduced_heisenberg_observables(large_ref_bitstring)

large_observables = [
    SparsePauliOp("I" * large_num_qubits + "X"),
    SparsePauliOp("I" * large_num_qubits + "Y"),
    large_obs_x_shifted_hamiltonian,
    large_obs_y_shifted_hamiltonian,
]
large_observable_labels = [
    "Re S_0d",
    "Im S_0d",
    "Re shifted H_0d",
    "Im shifted H_0d",
]

# -------------------------Step 2-------------------------

# Optimize the problem for quantum execution.
# Select a real backend and transpile the parameterized circuit to ISA form.
large_backend = service.backend("ibm_boston")
large_pass_manager = generate_preset_pass_manager(
    backend=large_backend, optimization_level=3, routing_method="none"
)
large_isa_circuit = large_pass_manager.run(large_extended_swap_test)
large_isa_observables = [
    observable.apply_layout(large_isa_circuit.layout)
    for observable in large_observables
]

large_two_qubit_gate_count = sum(
    instruction.operation.num_qubits == 2
    for instruction in large_isa_circuit.data
)

print(f"Backend: {large_backend.name}")
print(f"System qubits: {large_num_qubits}")
print(f"Total circuit qubits: {large_isa_circuit.num_qubits}")
print(f"Krylov dimension: {large_krylov_dim}")
print(f"Time step: {large_dt:.6f}")
print(f"Shift tau: {large_shift_tau}")
print(f"ISA circuit depth: {large_isa_circuit.depth()}")
print(f"ISA two-qubit gates: {large_two_qubit_gate_count}")
print(
    f"ISA two-qubit depth: {large_isa_circuit.depth(lambda x: x[0].num_qubits == 2)}"
)

# -------------------------Step 3-------------------------

# Execute on quantum hardware with Qiskit Runtime primitives.
# Submit one job per d, with all four observables in that job.
large_d_values = list(range(1, large_krylov_dim))
retrieve_batch_id = None
large_jobs = []

if retrieve_batch_id is None:
    large_estimator_options = {
        "default_shots": 8192,
        "dynamical_decoupling": {
            "enable": True,
            "sequence_type": "XpXm",
        },
        "resilience": {
            "measure_mitigation": True,
            "measure_noise_learning": {
                "num_randomizations": 32,
                "shots_per_randomization": 256,
            },
            "layer_noise_learning": {
                "max_layers_to_learn": 4,
                "layer_pair_depths": [0, 1, 2, 4, 16, 32],
                "num_randomizations": 32,
                "shots_per_randomization": 128,
            },
            "zne_mitigation": True,
            "zne": {
                "amplifier": "pea",
                "noise_factors": [1.0, 1.5, 2.0],
                "extrapolator": ("exponential", "linear"),
            },
        },
        "twirling": {
            "enable_gates": True,
            "enable_measure": True,
            "num_randomizations": 32,
            "shots_per_randomization": 256,
            "strategy": "active-accum",
        },
    }

    with Batch(backend=large_backend) as large_batch:
        large_batch_id = large_batch.session_id
        large_estimator = EstimatorV2(
            mode=large_batch,
            options=large_estimator_options,
        )
        # Krylov quantum diagonalization of lattice Hamiltonians -> TUT_KQDOLH.
        large_estimator.options.environment.job_tags = ["TUT_KQDOLH"]

        for d in large_d_values:
            parameter_values = [d * large_dt]
            pubs_for_d = [
                (large_isa_circuit, observable, parameter_values)
                for observable in large_isa_observables
            ]
            job = large_estimator.run(pubs_for_d)
            large_jobs.append(job)
            print(
                f"Submitted d={d}: job_id={job.job_id()}, "
                f"PUBs={len(pubs_for_d)}"
            )

    print(f"Batch ID: {large_batch_id}")
else:
    large_batch_id = retrieve_batch_id
    large_jobs = service.jobs(
        session_id=large_batch_id,
        limit=None,
        descending=False,
    )

large_job_ids_by_d = {
    d: job.job_id() for d, job in zip(large_d_values, large_jobs)
}
print(f"Job IDs by d: {large_job_ids_by_d}")

# -------------------------Step 4-------------------------

# Post-process the quantum results and solve the classical GEVP.
# Reconstruct the first rows of S and the shifted Hamiltonian matrix.
large_s_row_est = np.zeros(large_krylov_dim, dtype=complex)
large_h_shifted_row_est = np.zeros(large_krylov_dim, dtype=complex)
large_s_row_est[0] = 1.0
large_h_shifted_row_est[0] = large_ref_energy - large_shift_tau

for d, job in zip(large_d_values, large_jobs):
    job_result = job.result()

    if len(job_result) != len(large_observables):
        raise RuntimeError(
            f"Expected {len(large_observables)} PUB results for d={d}, "
            f"but received {len(job_result)}."
        )

    expectation_values = [
        np.asarray(pub_result.data.evs).reshape(-1)[0]
        for pub_result in job_result
    ]

    large_s_row_est[d] = expectation_values[0] + 1j * expectation_values[1]
    large_h_shifted_row_est[d] = (
        expectation_values[2] + 1j * expectation_values[3]
    )

# H_0d = shifted_H_0d + tau * S_0d.
large_h_row_est = large_h_shifted_row_est + large_shift_tau * large_s_row_est

large_s_matrix_est = la.toeplitz(
    large_s_row_est.conj(),
    large_s_row_est,
)
large_h_matrix_est = la.toeplitz(
    large_h_row_est.conj(),
    large_h_row_est,
)

large_exact_gnd = min(diagonalize_single_1_subspace(large_hamiltonian))

large_energy_convergence = []

with np.printoptions(precision=5, suppress=True):
    print("Estimated first row of S:")
    print(large_s_row_est)
    print("Estimated first row of H:")
    print(large_h_row_est)
    print("exact ground state energy:  ", large_exact_gnd)

    for r in range(1, large_krylov_dim + 1):
        energy_est_kqd, coeffs_est, retained_est = solve_thresholded_gevp(
            large_h_matrix_est[:r, :r],
            large_s_matrix_est[:r, :r],
            threshold=5e-2,
        )
        large_energy_convergence.append(energy_est_kqd)
        print(
            f"Krylov ground state energy (dim={r}, retained={retained_est}): ",
            energy_est_kqd,
        )

plot_energy_convergence(
    large_energy_convergence, large_exact_gnd, large_krylov_dim
)

Output:

Backend: ibm_boston
System qubits: 30
Total circuit qubits: 156
Krylov dimension: 7
Time step: 0.036110
Shift tau: 25.0
ISA circuit depth: 219
ISA two-qubit gates: 728
ISA two-qubit depth: 52
Job IDs by d: {1: 'd9fl4p4jeosc73fk4dg0', 2: 'd9fl4pineu4c739poecg', 3: 'd9fl4q2neu4c739poedg', 4: 'd9fl4qhhtsac739fjgn0', 5: 'd9fl4r4jeosc73fk4dhg', 6: 'd9fl4rkjeosc73fk4dj0'}
Estimated first row of S:
[ 1.     +0.j       0.42055-0.65466j -0.17883-0.73125j -0.64523-0.31601j
 -0.70781+0.55574j -0.02467+0.70525j  0.59301+0.55602j]
Estimated first row of H:
[ 25.      +0.j       10.3419 -16.5586j   -5.017  -18.03933j
 -16.44797 -6.98173j -17.17081+14.84502j   0.62722+17.81196j
  15.68886+12.74875j]
exact ground state energy:   21.021912418526902
Krylov ground state energy (dim=1, retained=1):  25.0
Krylov ground state energy (dim=2, retained=2):  24.432409110686205
Krylov ground state energy (dim=3, retained=3):  24.256856893278556
Krylov ground state energy (dim=4, retained=4):  23.727409826799715
Krylov ground state energy (dim=5, retained=4):  23.324720470780864
Krylov ground state energy (dim=6, retained=5):  21.91957579005085
Krylov ground state energy (dim=7, retained=5):  21.548331214122644
Output of the previous code cell

付録:ハミルトニアン関数(スペクトルフィルタ)の観点

KQDの運用における主なワークフローは以下の通りである。リアルタイムで推演された状態からクリロフ基底を構築し、射影行列 H\mathcal{H} および S\mathcal{S} を推定し、GEVPを解く。 本付録では、KQDが機能する理由を説明する補完的な視点、すなわちハミルトニアン関数、あるいはスペクトルフィルタの観点 [3]、 [5] から、 同じ計算を改めて検討する。 ここでは、12キュービットのモデル、時間ステップ Δt\Delta t、および上記で既に得られたクリロフ解を再利用しており、新たな回路の実行は必要ありません。

エネルギー分布としての基準状態

ハミルトニアンが次のような固有値分解を持つとする

H=∑mEm ∣Em⟩ ⁣⟨Em∣,E0≤E1≤⋯ ,\begin{equation*} H=\sum_{m} E_m\,|E_m\rangle\!\langle E_m|, \qquad E_0\le E_1\le\cdots, \end{equation*}

エネルギー固有状態は ∣Em⟩|E_m\rangle である。 任意の参照状態は、この固有基底を用いて展開することができ、

∣ψ0⟩=∑mam ∣Em⟩,am=⟨Em∣ψ0⟩,\begin{equation*} |\psi_{0}\rangle=\sum_m a_m\,|E_m\rangle, \qquad a_m=\langle E_m|\psi_{0}\rangle, \end{equation*}

したがって、各エネルギー EmE_m において、 スペクトル重み pm=∣am∣2p_m=|a_m|^2 を持つことになる。 基準エネルギーは、この分布の平均値 ⟨H⟩0=∑mpmEm\langle H\rangle_{0}=\sum_m p_m E_m である。

一般的な nn -量子ビットハミルトニアンの固有値分解には指数関数的な計算コストがかかるため、この図はあくまで診断用のものであり、アルゴリズムの一部ではない。 しかしここでは、 同じn=12n=12 の問題について、低コストで計算を行うことができます。ハイゼンベルク・ハミルトニアンは総励起数を保存し、参照分布 ∣000001000000⟩|000001000000\rangle は単一の励起を持つため、そのスペクトル成分のすべては、 nn に比例して線形にのみ次元が増加する単一励起部分空間に含まれます。 したがって、単一励起の正確なブロック(上記でベンチマークとしてすでに使用済み)を再利用し、その部分空間内の参照分布 {(Em,pm)}\{(E_m, p_m)\} を読み取ります。

# Exact single-excitation-subspace decomposition of the reference state.
# This reuses make_heisenberg_hamiltonian / _basis_state_transition_amplitude_sparse
# and the n=12 `hamiltonian` and `ref_bitstring` defined in the small-scale example.
single_excitation_states = [1 << k for k in range(num_qubits)]

h_single = np.array(
    [
        [
            _basis_state_transition_amplitude_sparse(hamiltonian, bra, ket)
            for ket in single_excitation_states
        ]
        for bra in single_excitation_states
    ]
)
h_single = 0.5 * (h_single + h_single.conj().T)

subspace_evals, subspace_evecs = la.eigh(h_single)
subspace_evals = np.real(subspace_evals)

# Reference-state coordinates inside the single-excitation subspace.
ref_position = single_excitation_states.index(int(ref_bitstring, 2))
ref_in_subspace = np.zeros(num_qubits, dtype=complex)
ref_in_subspace[ref_position] = 1.0

# Amplitudes and spectral weights of the reference in the energy eigenbasis.
ref_eigen_amplitudes = subspace_evecs.conj().T @ ref_in_subspace
ref_spectral_weights = np.abs(ref_eigen_amplitudes) ** 2

print(f"Single-excitation subspace dimension: {num_qubits}")
print(f"Subspace ground-state energy:         {subspace_evals[0]:.6f}")
print(
    f"Reference energy (sum p_m E_m):       {np.sum(ref_spectral_weights * subspace_evals):.6f}"
)
print(f"Reference weight on subspace ground:  {ref_spectral_weights[0]:.6f}")

Output:

Single-excitation subspace dimension: 12
Subspace ground-state energy:         3.136297
Reference energy (sum p_m E_m):       7.000000
Reference weight on subspace ground:  0.163827

KQDは、この分布を再形成するフィルタを学習する

ハミルトニアン関数 f(H)f(H) は、スペクトル解析を通じて次のように定義される

f(H)=∑mf(Em) ∣Em⟩ ⁣⟨Em∣,\begin{equation*} f(H)=\sum_m f(E_m)\,|E_m\rangle\!\langle E_m|, \end{equation*}

言い換えれば、固有射影の重み付き和である。 これを参照データに適用すると、各スペクトル振幅が次のように再形成されます。 am→amf(Em)a_m \to a_m f(E_m) :

f(H) ∣ψ0⟩=∑mam f(Em) ∣Em⟩.\begin{equation*} f(H)\,|\psi_{0}\rangle=\sum_m a_m\,f(E_m)\,|E_m\rangle . \end{equation*}

もし ff が最低エネルギー点で鋭いピークを持つ場合(そうでない場合は f(E0)=1f(E_0)=1 および f(Em)≈0f(E_m)\approx 0 )、 f(H)f(H) は基底状態の射影として機能し、正規化された出力は(ほぼ)基底状態となる。 したがって、エネルギー領域における優れたローパススペクトルフィルタこそが、まさに我々が求めているものである。

KQDは、 ff をあらかじめ規定していません。 その代わりに、リアルタイム進化の基底においてフィルタを展開し、

fKQD(E)=∑ℓ=0r−1cℓ e−iℓΔtE,\begin{equation*} f_{\rm KQD}(E)=\sum_{\ell=0}^{r-1} c_\ell\, e^{-i\ell\Delta t E}, \end{equation*}

エネルギーの三角関数であり、その係数 {cℓ}\{c_\ell\} は、まさに前述で求めたGEVPの固有ベクトルそのものである。 したがって、レイリー商 c†Hc/c†Sc\mathbf{c}^\dagger\mathcal{H}\mathbf{c}/\mathbf{c}^\dagger\mathcal{S}\mathbf{c} を最小化することは、基準信号の励起状態の重みを最も効果的に抑制するフィルタを学習することと同じである。 rr のクリロフ次元を大きくすると、フィルタにさらなる自由度が与えられ、基底状態のエネルギーにおけるピークがより鋭くなります。

以下のヘルパー関数は、学習されたこのフィルタをエネルギー軸上で評価し、その結果を前述の手順で得られた参照分布に適用します。

def trigonometric_krylov_filter(
    coeffs: np.ndarray,
    energies: np.ndarray,
    time_step: float,
) -> np.ndarray:
    """Evaluate the learned Krylov filter f(E) = sum_l c_l exp(-i l dt E)."""
    values = np.zeros_like(energies, dtype=complex)
    for ell, coeff in enumerate(coeffs):
        values += coeff * np.exp(-1j * ell * time_step * energies)
    return values


def filtered_spectral_weights(
    weights: np.ndarray,
    filter_values: np.ndarray,
) -> np.ndarray:
    """Reshape spectral weights by |f(E)|^2 and renormalize."""
    reshaped = weights * np.abs(filter_values) ** 2
    return reshaped / np.sum(reshaped)


# Recover the KQD coefficients from the already-estimated projected matrices.
# The shift only moves H by tau * S, so it does not change the GEVP eigenvector;
# we solve at the full Krylov dimension used in the small-scale example.
_, kqd_coeffs, _ = solve_thresholded_gevp(
    h_matrix_est,
    s_matrix_est,
    threshold=1e-12,
)

filter_on_spectrum = trigonometric_krylov_filter(
    kqd_coeffs, subspace_evals, dt
)
filtered_weights = filtered_spectral_weights(
    ref_spectral_weights, filter_on_spectrum
)

print(f"Ground-state overlap  (reference): {ref_spectral_weights[0]:.4f}")
print(f"Ground-state overlap  (filtered):  {filtered_weights[0]:.4f}")
print(
    f"Mean energy           (reference): {np.sum(ref_spectral_weights * subspace_evals):.6f}"
)
print(
    f"Mean energy           (filtered):  {np.sum(filtered_weights * subspace_evals):.6f}"
)

Output:

Ground-state overlap  (reference): 0.1638
Ground-state overlap  (filtered):  0.9638
Mean energy           (reference): 7.000000
Mean energy           (filtered):  3.164503

フィルターとその柔軟性を視覚化する

まず、上記で使用したフル・クリロフ次元における学習済みフィルタを示し、次に次元 rr が増加するにつれて、その解像度がどのように向上していくかを追跡します。

各バーは、連続的なエネルギー軸上に、基準スペクトル重み pmp_m (処理前)とフィルタ処理後の重み pm∣fKQD(Em)∣2p_m|f_{\rm KQD}(E_m)|^2 (処理後)、および学習されたフィルタ強度 ∣fKQD(E)∣2|f_{\rm KQD}(E)|^2 を示しています。 このフィルタは、単一励起部分空間の中で最も低いエネルギーに重みを集中させる。これは、小規模な例においてKQD推定値が収束したのと同じエネルギーである。 なお、これは単一励起セクター内の基底状態であり、この励起保存参照状態にとって関連する対象となるものであって、全体的な基底状態ではないことに留意されたい。

fig, ax = plt.subplots(figsize=(8, 4))

visible = (ref_spectral_weights > 1e-4) | (filtered_weights > 1e-4)
ax.bar(
    subspace_evals[visible],
    ref_spectral_weights[visible],
    width=0.18,
    alpha=0.45,
    label="reference $p_m$",
)
ax.bar(
    subspace_evals[visible],
    filtered_weights[visible],
    width=0.14,
    alpha=0.9,
    label=r"filtered $p_m\,|f_{\rm KQD}(E_m)|^2$",
)

energy_grid = np.linspace(
    subspace_evals.min() - 0.5, subspace_evals.max() + 0.5, 800
)
filter_intensity = (
    np.abs(trigonometric_krylov_filter(kqd_coeffs, energy_grid, dt)) ** 2
)
filter_intensity /= filter_intensity.max()
ax.plot(
    energy_grid,
    filter_intensity,
    color="k",
    linewidth=2,
    label=r"$|f_{\rm KQD}(E)|^2$ (normalized)",
)

ax.axvline(
    subspace_evals[0],
    color="C3",
    linestyle="--",
    linewidth=1,
    label="subspace ground energy",
)
ax.set_xlabel("Energy eigenvalue $E_m$")
ax.set_ylabel("Spectral weight")
ax.set_ylim(0, 1)
ax.legend(loc="upper right")
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

クリロフ次元の増加:学習済み関数の柔軟性

学習されたフィルタは、エネルギーに関する三角多項式であり、 rr の係数を持つことを思い出してください

fKQD(E)=∑ℓ=0r−1cℓ e−iℓΔtE.\begin{equation*} f_{\rm KQD}(E)=\sum_{\ell=0}^{r-1} c_\ell\, e^{-i\ell\Delta t E}. \end{equation*}

クリロフ次元 rr は自由係数の数と正確に一致するため、関数の柔軟性を制御します。 rr が小さい場合、フィルタは広域で緩やかに変化する特性しか得られず、低エネルギーの励起状態にも重みが漏れ出してしまう。 rr が増加すると、フィルタは目標エネルギーにおいてより狭いピークを形成し、残りの励起状態の重みをより強力に抑制することができる。 これは、小規模な例で観察されたエネルギーの収束に対応するスペクトルフィルタの効果です。 rr が大きくなるにつれて、フィルタ処理された分布は部分空間の基底状態に収束し、推定エネルギーもその値に向かって減少していきます。

上記で既に推定された投影行列を再利用し、各主要な r×rr\times r ブロックにおいてGEVPを単純に解き、その後、対応するフィルタを評価してプロットします。

# Sweep the Krylov dimension using the leading r x r blocks of the estimated matrices.
sweep_dims = [r for r in (2, 4, 6, 8, krylov_dim) if r <= krylov_dim]
sweep_dims = sorted(set(sweep_dims))

energy_grid = np.linspace(
    subspace_evals.min() - 0.5, subspace_evals.max() + 0.5, 800
)

sweep_cases = []
print(" r    retained    ground overlap    filtered energy")
print("--    --------    --------------    ---------------")
for r in sweep_dims:
    _, coeffs_r, retained_r = solve_thresholded_gevp(
        h_matrix_est[:r, :r],
        s_matrix_est[:r, :r],
        threshold=1e-12,
    )
    filter_on_spectrum_r = trigonometric_krylov_filter(
        coeffs_r, subspace_evals, dt
    )
    filtered_weights_r = filtered_spectral_weights(
        ref_spectral_weights, filter_on_spectrum_r
    )
    filtered_energy_r = float(np.sum(filtered_weights_r * subspace_evals))

    sweep_cases.append((r, coeffs_r, filtered_weights_r))
    print(
        f"{r:2d}    {retained_r:8d}    {filtered_weights_r[0]:14.4f}    {filtered_energy_r:15.6f}"
    )

print(f"\nSubspace ground-state energy (target): {subspace_evals[0]:.6f}")

# One panel per Krylov dimension: filtered spectrum (bars) + filter intensity (curve).
fig, axes = plt.subplots(
    len(sweep_cases),
    1,
    figsize=(8, 2.1 * len(sweep_cases)),
    sharex=True,
)
axes = np.atleast_1d(axes)

for idx, (ax, (r, coeffs_r, filtered_weights_r)) in enumerate(
    zip(axes, sweep_cases)
):
    visible = (ref_spectral_weights > 1e-4) | (filtered_weights_r > 1e-4)
    ax.bar(
        subspace_evals[visible],
        ref_spectral_weights[visible],
        width=0.18,
        alpha=0.35,
        color="C0",
        label="reference $p_m$" if idx == 0 else None,
    )
    ax.bar(
        subspace_evals[visible],
        filtered_weights_r[visible],
        width=0.14,
        alpha=0.9,
        color="C1",
        label="filtered weights" if idx == 0 else None,
    )

    filter_intensity_r = (
        np.abs(trigonometric_krylov_filter(coeffs_r, energy_grid, dt)) ** 2
    )
    filter_intensity_r /= filter_intensity_r.max()
    ax.plot(energy_grid, filter_intensity_r, color="k", linewidth=2)

    ax.axvline(subspace_evals[0], color="C3", linestyle="--", linewidth=1)
    ax.set_ylim(0, 1)
    ax.set_ylabel("weight")
    ax.legend(loc="upper right", title=f"$r={r}$")

axes[-1].set_xlabel("Energy eigenvalue $E_m$")
fig.suptitle(
    r"KQD-learned filter $|f_{\rm KQD}(E)|^2$ sharpening with Krylov dimension $r$",
    y=1.0,
)
fig.tight_layout()
plt.show()

Output:

 r    retained    ground overlap    filtered energy
--    --------    --------------    ---------------
 2           2            0.4549           4.184510
 4           4            0.8373           3.310168
 6           6            0.9706           3.158100
 8           7            0.9701           3.158725
10           8            0.9638           3.164503

Subspace ground-state energy (target): 3.136297
Output of the previous code cell

次のステップ

この作品に興味を持たれた方は、以下の資料もご参考になるかもしれません:


参照

[1] E. N. エッパーリー、L. リン、および Y. 中司、 量子部分空間の対角化に関する理論、 *『SIAM Journal on Matrix Analysis and Applications』*43 、 1263-1290(2022年)。

[2] N. 吉岡、M.、アミコ、W.、カービー、ほか、 量子プロセッサ上での大規模多体ハミルトニアンの対角化、『 arXiv:2407.14431 』(2024年)。

[3] R. M. Parrish および P. L. McMahon, 「量子フィルタの対角化:完全な量子位相推定を必要としない量子固有値分解 」,『 Physical Review Letters』** 122**, 230401 (2019)。

[4] G. Lee, S. Choi, J. Huh、および AF Izmaylov、 「量子クリロフ部分空間対角化におけるサンプリング誤差を低減するための効率的な戦略」、 Digital Discovery 4、954-969 (2025)。

[5] G. Lee, M.、Kang, J.、Hong, S.、Fomichev, J. および J. 「ふむ、 フィルタ付き量子位相推定 」、 arXiv:2510.04294 (2025)。

このページは役に立ちましたか?
バグや誤字の報告、またはコンテンツの要求はGitHubで行ってください。