Skip to main content
IBM Quantum Platform

Krylov-Quanten-Diagonalisierung von Gitter-Hamilton-Operatoren

Geschätzte Laufzeit: 70 Minuten auf einem Heron- oder Nighthawk-Prozessor (HINWEIS: Dies ist lediglich eine Schätzung. (Die Laufzeit kann variieren.)


Lernziele

  • Wie lässt sich die Krylov-Quanten-Diagonalisierung (KQD) als das Erlernen einer endlichen Hamilton-Funktion interpretieren, die als Spektralfilter fungiert?
  • Wie man die projizierten Hamilton- und Überlappungsmatrizen mithilfe erweiterter Swap-Test-Messungen erstellt.
  • Wie lässt sich das daraus resultierende verallgemeinerte Eigenwertproblem (GEVP) lösen und eine Schätzung der Grundzustandsenergie für einen Gitter-Hamilton-Operator ermitteln?

Voraussetzungen


Hintergrund

Dieses Tutorial zeigt, wie man den Krylov-Quanten-Diagonalisierungsalgorithmus (KQD) im Rahmen von Qiskit-Patterns implementiert. Zunächst lernen Sie die Theorie hinter dem Algorithmus kennen und sehen anschließend eine Demonstration seiner Ausführung auf einer QPU.

Die Abschätzung der Eigenschaften von Vielteilchen-Hamilton-Operatoren im Niedrigenergiebereich ist eine zentrale Aufgabe der Quantensimulation. So stehen beispielsweise Grundzustandsenergien und niedrigliegende Anregungen in direktem Zusammenhang mit der chemischen Stabilität, der magnetischen Ordnung, Quantenphasenübergängen und dem Materialverhalten. Auf einem klassischen Computer wächst die Dimension des Hilbert-Raums exponentiell mit der Anzahl der Orbitalen oder Spins, sodass eine direkte Diagonalisierung schnell unpraktikabel wird.

Für dieses Problem gibt es mehrere Ansätze im Bereich des Quantencomputings. Kurzfristige Variationsmethoden wie der Variational Quantum Eigensolver (VQE) verwenden relativ flache parametrisierte Schaltkreise, erfordern jedoch eine nichtlineare klassische Optimierungsschleife mit zahlreichen Auswertungen der Quantenschaltkreise. Auf der anderen Seite bietet die Quantenphasenschätzung (QPE) einen direkteren Weg zur Eigenwertschätzung mit strengen Garantien, doch die Standard-QPE erfordert lange kohärente Schaltkreise und eignet sich vor allem für fehlertolerante Quantencomputer. KQD liegt zwischen diesen beiden Ansätzen: Es nutzt die Hamilton-Evolution in Echtzeit, wie bei Algorithmen auf Basis der Phasenschätzung, ersetzt jedoch die vollständige Phasenschätzung durch ein kompaktes projiziertes Eigenwertproblem, das klassisch gelöst werden kann.

Betrachten wir einen nn -Qubit-Hamilton-Operator HH und einen Referenzzustand ∣ψ0⟩\lvert \psi_{0}\rangle. Die KQD-Methode erstellt einen Krylov-Unterraum aus in Echtzeit entwickelten Zuständen,

∣ψℓ⟩=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*}

wobei rr die Krylov-Dimension und Δt\Delta t der Zeitschritt ist. Jeder Zustand im Krylov-Unterraum lässt sich dann als Linearkombination dieser Basiszustände darstellen,

∣ψ(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*}

wobei der Nenner den Zustand normiert.

Mit einfacher Algebra lässt sich zeigen, dass die entsprechende Energie als Rayleigh-Quotient ausgedrückt wird:

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*}

Hier sind die Matrizen S\mathcal{S} und 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*}

Definieren Sie die projizierte Überlappungsmatrix und die Hamilton-Matrix. Ihre Einträge werden anhand von Messungen an Quantenschaltungen geschätzt.

Wir möchten den Koeffizienten c\mathbf{c} ermitteln, der das Minimum E(c)E(\mathbf{c}) liefert:

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

Nach dem Rayleigh-Ritz-Satz entspricht diese Minimierung der Lösung des verallgemeinerten Eigenwertproblems (GEVP),

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

Beachten Sie, dass die Dimension rr klein genug sein kann, damit ein klassischer Computer das GEVP lösen kann.

Es handelt sich um dasselbe Variationsprinzip, das auch bei der klassischen Unterraumdiagonalisierung zum Einsatz kommt, doch hier werden die Basiszustände durch die Quantenzeitentwicklung erzeugt. Im Vergleich zu VQE erfordert KQD in der Regel tiefere Schaltkreise, da es auf Echtzeit-Entwicklung basiert. Im Gegenzug vermeidet KQD die nichtlineare Parameteroptimierung und die iterative Ausführung auf Quantenhardware und verbessert sich systematisch mit zunehmender Größe des projizierten Unterraums. Der Algorithmus wurde in großem Maßstab auf bestehender Quantenhardware demonstriert [2], und seine Leistungsfähigkeit lässt sich mit nachweisbaren Garantien analysieren [1].


Anforderungen

Bevor Sie mit dieser Anleitung beginnen, stellen Sie bitte sicher, dass folgende Komponenten installiert sind:

  • Qiskit SDK v2.3 oder höher mit Visualisierungsunterstützung
  • Qiskit Runtime v0.22 oder höher (pip install qiskit-ibm-runtime)
  • SciPy (pip install scipy)
  • Matplotlib (pip install matplotlib)
  • Pandas (pip install pandas)

Für die Ausführung auf der Hardware sind qiskit-ibm-runtime sowie der Zugriff auf ein „ IBM Quantum® “-Konto erforderlich.


Einrichtung

Die Setup-Zelle importiert die erforderlichen Module und definiert Hilfsfunktionen für den Workflow:

  1. den Heisenberg-Hamilton-Operator aufstellen;
  2. das GEVP mit Schwellenwert lösen;
  3. den trainierten Krylov-Filter auswerten;
  4. Filterwerte in spektrale Gewichte umwandeln;
  5. Stellen Sie die Referenz- und die gefilterten Energieverteilungen grafisch dar.
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))

Im ersten Teil dieses Tutorials veranschaulichen wir die KQD-Methode anhand eines Simulators mit lokalem Zustandsvektor. Später nutzen wir ein echtes Quanten-Backend, um ein Problem im industriellen Maßstab anzugehen.

Außerdem definieren wir ein fiktives Backend, um die backendspezifische Transpilierung zu veranschaulichen und die daraus resultierende Schaltung zu untersuchen.

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

backend = FakeMarrakesh()

Beispiel für einen Simulator im kleinen Maßstab

Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen

Hamilton-Funktor und Referenzzustand

In diesem Beispiel wird eine Heisenberg-Kette mit 12 Qubits und offenen Grenzen verwendet ( 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*}

mit einem Produktzustand mit einer einzigen Anregung

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

als Referenzzustand. Da der oben definierte Heisenberg-Hamilton-Operator die Gesamtzahl der Anregungen bewahrt, verbleibt der Referenzzustand im Unterraum mit einer einzigen Anregung, dessen Dimension nur linear mit der Anzahl der Qubits wächst. Wir können daher die exakte Grundzustandsenergie effizient berechnen, indem wir den auf diesen Unterraum beschränkten Hamiltonoperator diagonalisieren, und sie ausschließlich als diagnostischen Maßstab für die KQD-Schätzung verwenden. Der KQD-Workflow selbst schätzt die projizierten Matrixelemente mithilfe von „ Qiskit primitives “ und löst das daraus resultierende projizierte Problem auf klassische Weise.

# 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)

Legen Sie die Parameter für den Algorithmus fest

Ausgehend von den Obergrenzen für die Hamilton-Norm schlägt Ref. [1] heuristisch die Zeitschritt Δt\Delta t als „ π/∥H∥\pi/\|H\| “ vor. Da die Spektralnorm ∥H∥\|H\| schwer zu berechnen ist, verwenden wir stattdessen ihre Obergrenze:

∥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*}

Wir haben die Krylov-Dimension auf r=10r=10 und die Anzahl der Trotter-Schritte pro Zeitschritt auf 55 festgelegt: ein ausreichend großer Krylov-Raum, um das tiefliegende Spektrum aufzulösen und gleichzeitig die tiefste Schaltung ( tmax⁡=(r−1)Δtt_{\max}=(r-1)\Delta t ) rechnerisch überschaubar zu halten, sowie genügend Trotter-Schritte, um den Diskretisierungsfehler bei dieser tiefsten Schaltung gering zu halten.

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

Schaltung aufbauen

Hier bauen wir die Schaltungen auf, um die Matrixelemente Hkℓ\mathcal{H}_{k\ell} und Skℓ\mathcal{S}_{k\ell} zu schätzen. Da alle Potenzen von HH kommutieren, gilt

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*}

Solche Matrizen, bei denen die Elemente auf diese Weise von den Indexunterschieden abhängen, werden als Toeplitz-Matrizen bezeichnet und lassen sich aus den Elementen der ersten Zeile rekonstruieren, die durch d=ℓ−kd=\ell-k indiziert sind.

Hier stellen wir die Schaltung vor, die als „Extended-Swap-Test“ bezeichnet wird und die

∣Φ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*}

wobei ∣ψd⟩=e−idΔtH∣ψ0⟩|\psi_d\rangle=e^{-id\Delta tH}|\psi_{0}\rangle gilt.

Referenzstatus

Wir berechnen den Referenzzustand ∣ψ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

Zeitliche Entwicklung

Berechnen Sie den durch den Hamilton-Operator erzeugten Zeitentwicklungsoperator, der durch eine einfache Lie-Trotter-Approximation approximiert wird.

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

Erweiterte Swap-Testschaltung [3]

Die Schaltung legt zunächst den Referenzzustand im Systemregister fest, während sich die Ancilla weiterhin im Zustand „ ∣0⟩|0\rangle “ befindet:

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

Wendet man dann ein Hadamard-Gatter auf die Ancilla an, entsteht eine kohärente Überlagerung zweier Verzweigungen:

∣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*}

Schließlich gilt das Gatter für die kontrollierte Zeitentwicklung:

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

nur wenn sich die Ancilla im Zweig „ ∣1⟩|1\rangle “ befindet. Dies bedeutet Folgendes:

∣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*}

Im nächsten Code-Block implementieren wir:

∣Ψ(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*}

die im Ausführungsschritt als „ t=dΔtt=d\Delta t “ für „ d=1,⋯r−1d=1,\cdots r-1 “ zugewiesen wird.

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

Beobachtbare Größen

Für jedes hermitische System mit den Observablen OO legen wir hier die zu berechnenden Observablen wie folgt fest:

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

Das liegt daran, dass O=IO=I das Überlappungselement S0d=⟨ψ0∣ψd⟩\mathcal{S}_{0d}=\langle\psi_0|\psi_d\rangle liefert, während O=HO=H das Hamilton-Element H0d=⟨ψ0∣H∣ψd⟩\mathcal{H}_{0d}=\langle\psi_0|H|\psi_d\rangle liefert.

Unter Verwendung von X=∣0⟩⟨1∣+∣1⟩⟨0∣X=|0\rangle\langle 1|+|1\rangle\langle 0| erhalten wir

⟨Ψ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*}

Ebenso lässt sich mit „ 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*}

Daher gilt:

⟨Φ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*}

Schließlich müssen wir für jeden Zustands ∣Ψ0d⟩|\Psi_{0d}\rangle Folgendes messen:

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])

Bei der Abschätzung der Matrixelemente von „ H\mathcal{H} “ ist die Anzahl der Pauli-Terme wesentlich größer als bei den Matrixelementen von „ S\mathcal{S} “.

Mithilfe einer Verschiebungstechnik [4] können wir nun die Anzahl der gemessenen Hamilton-Terme reduzieren. Wir zerlegen den Hamilton-Operator wie folgt:

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

wobei TT so gewählt wird, dass der Referenzzustand sein Eigenzustand ist,

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

Dann,

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*}

In dieser Datei gilt Folgendes:

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

ist das verschobene Matrixelement des Hamilton-Operators. Daher müssen wir lediglich X⊗(H−T)X\otimes (H-T) und Y⊗(H−T)Y\otimes (H-T) messen. Der Beitrag von TT wird klassisch anhand des bereits gemessenen Überlappungsmatrixelements S0d\mathcal{S}_{0d} rekonstruiert.

In diesem Beispiel bietet sich der diagonale Teil des Heisenberg-Hamilton-Operators an,

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

Da es sich bei dem Referenzzustand um einen Berechnungsbasiszustand handelt, ist er ein Eigenzustand jedes „ ZiZi+1Z_iZ_{i+1} “-Terms.

Eine vorteilhaftere Wahl ist es jedoch, nicht nur die diagonalen ZZZZ Terme einzubeziehen, sondern auch die XX+YYXX+YY Terme, die den Referenzzustand auslöschen.

Für jedes Nachbarpaar gilt für den Operator 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*}

und

(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*}

Daher trägt der Term „ XX+YYXX+YY “ nur dann bei, wenn die beiden benachbarten Qubits unterschiedliche Belegungen in der Referenz-Bitfolge aufweisen. Sind die beiden Qubits entweder beide „ 00 “ oder beide „ 11 “, so löscht dieser Term den Referenzzustand aus und kann ebenfalls herausgenommen werden.

Sei ∣ψref⟩=∣z0⋯zn−1⟩|\psi_{\rm ref}\rangle = |z_0\cdots z_{n-1}\rangle, wobei zi∈{0,1}z_i \in \{0,1\} gilt. Somit können wir wählen

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*}

Dieser Operator erfüllt weiterhin

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

denn die Terme von ZZZZ wirken diagonal auf ∣ψ0⟩|\psi_0\rangle ein, während die verschobenen Terme von XX+YYXX+YY den Wert Null ergeben. Der entsprechende Eigenwert wird daher ausschließlich durch die Terme „ ZZZZ “ bestimmt,

τ=∑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*}

Mit dieser Wahl lautet der verschobene Hamilton-Operator nun:

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*}

Daher müssen nur die Kanten gemessen werden, an denen im Referenzzustand unterschiedliche Berufe ausgeübt werden. Alle Terme von ZZZZ und alle inaktiven Terme von XX+YYXX+YY werden durch den Überlappungsbeitrag τS0d\tau \mathcal{S}_{0d} rekonstruiert oder liefern per Konstruktion einen Beitrag von Null.

Dadurch ergibt sich eine kleinere Observable als bei einer Verschiebung nur des diagonalen Teils. Insbesondere bleiben bei einem Referenzzustand auf Berechnungsbasis mit einer lokalisierten Anregung nur die an die Anregung angrenzenden Kanten in H−TH-T erhalten. Daher kann die Anzahl der Pauli-Terme in X⊗(H−T)X\otimes(H-T) und Y⊗(H−T)Y\otimes(H-T) erheblich reduziert werden, während das rekonstruierte Matrixelement

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

bleibt genau gleich.

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

Schritt 2: Optimierung des Problems für die Ausführung auf Quantenhardware

Nun wandeln wir die abstrakte Schaltung für den erweiterten Swap-Test in eine hardwareorientierte Vorlage um. Zuvor optimieren wir die Schaltung zunächst auf abstrakter Ebene weiter.

Vergleich der Reihenfolge der Hamilton-Terme

Zunächst vergleichen wir verschiedene Reihenfolgen der Pauli-Terme im Heisenberg-Hamilton-Operator für die Hamilton-Simulation. Der Hamilton-Operator selbst bleibt unverändert, doch die Reihenfolge beeinflusst, wie der Schaltkreis in Produktform generiert wird und inwieweit die Struktur im Schaltkreis parallelisiert werden kann. Beispielsweise listet die naive Reihenfolge zunächst alle „Nearest-Neighbor“- XXXX -Terme auf, dann alle „ YYYY “-Terme und schließlich alle „ ZZZZ “-Terme. Dadurch werden benachbarte Kanten wie (0,1)(0,1) und (1,2)(1,2) nebeneinander platziert, sodass sie nicht parallel ausgeführt werden können. Bei der „even-then-odd“-Reihenfolge werden zunächst die disjunkten geraden Kanten und anschließend die ungeraden Kanten durchlaufen, wodurch parallele Zwei-Qubit-Schichten freigelegt werden. Die nach geraden und ungeraden Kanten gruppierte Reihenfolge geht noch einen Schritt weiter: Für jede Kante werden die lokalen Terme „ XXXX “, „ YYYY “ und „ ZZZZ “ beibehalten, während weiterhin gerade Kanten vor ungeraden Kanten besucht werden. Wir gehen davon aus, dass die „even-then-odd“- und „even-odd“-Anordnungen mit Kantengruppierung die Schaltungstiefe verringern, indem sie parallele Zwei-Qubit-Schichten freilegen, und dass die Anordnung mit Kantengruppierung zudem den Trotter-Fehler reduziert, da die lokale Zwei-Qubit-Wechselwirkung an derselben Kante als kompakter Block behandelt wird.

Pauli-Entwicklungen mit unterschiedlicher Reihenfolge der Terme

Die Hamilton-Reihenfolge wirkt sich auch auf den Trotter-Fehler aus. Wenn wir nichtkommutierende Terme nebeneinander setzen, treten die Basisübergänge häufiger auf, was zu einem größeren Trotter-Fehler führt. Durch die Gruppierung von Termen, die dieselbe Pauli-Basistransformation erfordern, lassen sich überflüssige Basiswechsel vermeiden.

Hier wird für den Vergleich die größte Evolutionszeit herangezogen, die in den Krylov-Schätzungen der ersten Zeile auftritt ( tmax⁡=(r−1)Δtt_{\max} = (r-1)\Delta t ), wobei dieselbe Transpilationsbedingung verwendet wird.

Um den Trotter-Fehler zu messen, verwenden wir die Prozessuntreue zwischen der Trotterisierten Schaltung und der exakten Hamilton-Entwicklung,

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*}

wobei d=2nd=2^n die Dimension des Hilbert-Raums ist.

Diese Diagnose verwendet dichte Matrizen und eignet sich daher für dieses kleine 12-Qubit-Beispiel, ist jedoch nicht als skalierbare Subroutine gedacht.

# 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

Zunächst belassen wir die Syntheseregel auf einen Trotter-Schritt erster Ordnung und variieren lediglich die Reihenfolge der Pauli-Terme. Das Ziel dieses Vergleichs besteht vor allem darin, zu ermitteln, um wie viel die Schaltungstiefe und die Kosten pro zwei Qubits reduziert werden können, wenn nicht überlappende Kanten zwischen nächsten Nachbarn dem Transpiler zugänglich gemacht werden.

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 ,

Wir stellen fest reps=1, dass sowohl die „even-then-odd“- als auch die „even-odd“-Kantengruppierung die Tiefe von 15 auf 6 reduzieren, indem sie parallele Zwei-Qubit-Schichten freilegen, während die Anzahl der Zwei-Qubit-Gatter bei allen drei Anordnungen gleich bleibt. Da jedoch bei allen drei Ordnungen die Untreue nahe bei 1 liegt, variieren wir die Anzahl der Trotter-Wiederholungen, um die Ordnungen deutlicher voneinander abzugrenzen. Mit zunehmender Anzahl von Wiederholungen nimmt die Unzuverlässigkeit der Reihenfolge even-odd edge-grouped schneller ab als bei den beiden anderen und erreicht den Wert 0.045, während reps=8 sie bei der naiven sowie der „zuerst gerade, dann ungerade“-Reihenfolge bei 0.075 liegt.

Vergleich der Synthese von Produktformeln

Als Nächstes untersuchen wir verschiedene erweiterte Einstellungen der Trotterisierung, wobei die feste Hamilton-Ordnung auf die nach geraden und ungeraden Kanten gruppierte Ordnung umgestellt wird. Wir betrachten den Lie-Trotter-Prozess erster Ordnung, den Suzuki-Trotter-Prozess zweiter Ordnung und den Suzuki-Trotter-Prozess vierter Ordnung.

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 liefert den flachsten Kreis, weist jedoch den größten Fehler auf, während der Suzuki-Trotter-Algorithmus vierter Ordnung zwar genauer ist, aber die Kreistiefe erhöht. Für den weiteren Verlauf des Tutorials entscheiden wir uns für die Suzuki-Trotter-Formel zweiter Ordnung, da sie eine Schaltung mit geringer Tiefe liefert und gleichzeitig den Trotter-Fehler im Vergleich zur Formel erster Ordnung erheblich reduziert.

Das Gatter für die kontrollierte zeitliche Entwicklung entfernen

Im erweiterten Swap-Test erfordert die Steuerung der zeitlichen Entwicklung durch ein einzelnes Ancilla-Qubit, dass das Ancilla viele Gatter im gesamten System steuert. Dies kann zu einem erheblichen Routing-Aufwand führen und erfordert im schlimmsten Fall praktisch eine „All-to-One“-Konnektivität. Um dies zu vermeiden, ist eine weitere Optimierung möglich, indem das gesteuerte Zeitentwicklungsgatter durch eine steuerungsfreie Version ersetzt wird, wobei die Symmetrie des Hamilton-Operators ausgenutzt wird. Betrachten wir die folgende Schaltung.

Schaltungsoptimierung

Hier bereitet BrefB_{\rm ref} den Referenzzustand vor: Bref∣0n⟩=∣ψ0⟩B_{\rm ref}|0^n\rangle = |\psi_{0}\rangle.

Anstatt zunächst „ 12(∣0⟩+∣1⟩)∣ψ0⟩\frac{1}{\sqrt{2}}(|0\rangle+|1\rangle)|\psi_0\rangle “ vorzubereiten und anschließend „ U(t)U(t) “ nur auf den Zweig „ ∣1⟩|1\rangle “ anzuwenden, bereitet der Schaltkreis die beiden Zweige direkt wie folgt vor:

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

wo

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

Die Schaltung wendet zunächst ein Hadamard-Gatter auf den Ancilla an und bereitet den Referenzzustand nur auf dem Zweig „ ∣1⟩|1\rangle “ vor:

∣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*}

Anschließend wird der Operator der unkontrollierten Zeitentwicklung auf beide Zweige angewendet:

∣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*}

Da der Hamiltonoperator die Anregungszahl erhält, lässt sich erkennen, dass ∣0n⟩|0^n\rangle sein Eigenzustand ist, und somit akumuliert der Evolutionsoperator unter dem Hamiltonoperator lediglich eine Phase:

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

Dies bedeutet Folgendes:

∣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*}

Anschließend wird „ BrefB_{\rm ref} “ ausschließlich auf den Zweig „ ∣0⟩|0\rangle “ angewendet.

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*}

Zu diesem Zeitpunkt weisen die beiden Zweige eine zusätzliche relative Phase auf. Um es zu entfernen, wenden wir das Ancilla-Phasengatter an

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*}

Dadurch wird der Zustand wie folgt transformiert:

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*}

So haben wir schließlich, bis auf eine irrelevante globale Phase, Folgendes hergestellt:

∣Φ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*}

Im folgenden Code-Block implementieren wir diese steuerungsfreie Schaltung.

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

Transpilation

Nun transpilieren wir die gesteuerten und die ungesteuerten Schaltungen, damit sie auf der Hardware ausgeführt werden können. Vergleichen wir das Ergebnis der transpilierten Schaltungen.

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>
})

Schritt 3: Mit „ Qiskit primitives “ ausführen

Der nächste Schritt besteht darin, dieselbe parametrisierte Schaltung für mehrere Werte von dd zu betrachten. Für jeden Wert von dd schätzen wir vier Erwartungswerte: X⊗IX\otimes I, Y⊗IY\otimes I, X⊗(H−T)X\otimes (H-T) und Y⊗(H−T)Y\otimes (H-T). Diese vier Zahlen werden dann zu den komplexen Elementen der ersten Zeile S0d\mathcal{S}_{0d} und H0d\mathcal{H}_{0d} zusammengefasst.

Hier lassen sich S00=1\mathcal{S}_{00}=1 und 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 klassisch berechnen, da ∣ψ0⟩\lvert\psi_0\rangle spärlich ist; daher überspringen wir den Fall 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']

Schritt 4: Nachbearbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format

Nach der Schätzung der projizierten Matrizen führen wir eine Regularisierung durch und lösen das GEVP

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

Der kleinste verallgemeinerte Eigenwert liefert die KQD-Schätzung der Grundzustandsenergie.

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

Nun lösen wir das verallgemeinerte Eigenwertproblem unter Verwendung der Matrizen, die aus den Schaltungsschätzungen rekonstruiert wurden. Bei einer idealen Zustandsvektorberechnung mit exakter Echtzeitentwicklung sollte dies das exakte projizierte Ergebnis wiedergeben. In der Praxis können Abweichungen durch Trotterisierung, Stichprobenfehler und numerische Instabilität der Überlappungsmatrix entstehen.

Wir beobachten, wie sich die Energie annähert, wenn wir die Dimension des Krylov-Unterraums erhöhen.

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

Beispiel für groß angelegte Hardware

Im vorangegangenen Abschnitt wurde ein 12-Qubit-Modell verwendet, damit die Zustandsvektor-Simulation als Diagnoseinstrument dienen konnte. Nun skalieren wir denselben KQD-Workflow auf eine Heisenberg-Kette mit 30 Qubits und bereiten die Arbeitslast für die Ausführung auf der „ IBM Quantum “-Hardware vor.

Schritte 1–4 in einem einzigen Codeblock zusammengefasst

Hier führen wir nun all diese Details zu einem einzigen Workflow in größerem Maßstab zusammen, der anschließend auf unserer realen Quantenhardware ausgeführt wird. In diesem Abschnitt wenden wir realistische Einstellungen zur Fehlerminderung an, um die Zuverlässigkeit der Ergebnisse zu verbessern. Da die Matrixelemente, die verschiedenen Werten von dd entsprechen, parallel berechnet werden können, nutzen wir den Modus mode Batch, um sie effizient auszuführen.

# -------------------------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

Anhang: Die Sichtweise der Hamilton-Funktion (Spektralfilter)

Der vorgestellte Hauptablauf veranschaulicht die praktische Anwendung von KQD: Erstellung einer Krylov-Basis aus in Echtzeit berechneten Zuständen, Schätzung der projizierten Matrizen H\mathcal{H} und S\mathcal{S} sowie Lösung des GEVP. In diesem Anhang wird dieselbe Berechnung aus einem ergänzenden Blickwinkel betrachtet, der erklärt, warum KQD funktioniert: aus der Perspektive der Hamilton-Funktion bzw. des Spektralfilters [3], [5]. Es greift auf das 12-Qubit-Modell, die Zeitschritt- Δt\Delta t und die oben bereits ermittelte Krylov-Lösung zurück; eine erneute Schaltungsausführung ist nicht erforderlich.

Der Referenzzustand als Energieverteilung

Sei die Eigenwertzerlegung des Hamilton-Operators

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*}

mit den Energieeigenzuständen ∣Em⟩|E_m\rangle. Jeder Referenzzustand lässt sich in dieser Eigenbasis entwickeln,

∣ψ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*}

Daher weist sie bei jedem Energie EmE_m ein Spektralgewicht pm=∣am∣2p_m=|a_m|^2 auf. Die Referenzenergie ist der Mittelwert dieser Verteilung, ⟨H⟩0=∑mpmEm\langle H\rangle_{0}=\sum_m p_m E_m.

Die Eigenwertzerlegung eines allgemeinen Hamilton-Operators vom Typ „ nn “-Qubit ist exponentiell aufwendig, daher dient dieses Bild lediglich der Diagnose und ist nicht Bestandteil des Algorithmus. Hier können wir dies jedoch für dasselbe Problem „ n=12n=12 “ kostengünstig berechnen: Der Heisenberg-Hamiltonoperator bewahrt die Gesamtanzahl der Anregungen, und die Referenzverteilung ∣000001000000⟩|000001000000\rangle weist eine einzige Anregung auf, sodass ihr gesamter Spektralinhalt im Unterraum der einzelnen Anregung liegt, dessen Dimension nur linear mit nn wächst. Wir verwenden daher erneut den exakten Block für eine einzelne Anregung (der oben bereits als Benchmark verwendet wurde) und lesen die Referenzverteilung {(Em,pm)}\{(E_m, p_m)\} innerhalb dieses Unterraums ab.

# 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 trainiert einen Filter, der diese Verteilung umformt

Eine Hamilton-Funktion f(H)f(H) wird mittels Spektralrechnung definiert,

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

oder, mit anderen Worten, eine gewichtete Summe von Eigenprojektoren. Wird dies auf die Referenz angewendet, wird jede Spektralamplitude umgestaltet, 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*}

Wenn „ ff “ bei der niedrigsten Energie eine scharfe Spitze aufweisen würde (ansonsten „ f(E0)=1f(E_0)=1 “ und „ f(Em)≈0f(E_m)\approx 0 “), würde „ f(H)f(H) “ als Grundzustands-Projektor fungieren, und die normierte Ausgabe wäre (nahezu) der Grundzustand. Daher ist ein guter energiespektraler Tiefpassfilter genau das, was wir brauchen.

KQD schreibt „ ff “ nicht im Voraus vor. Stattdessen erweitert es den Filter auf der Basis der Echtzeitentwicklung,

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*}

eine trigonometrische Funktion der Energie, deren Koeffizienten {cℓ}\{c_\ell\} genau dem oben ermittelten GEVP-Eigenvektor entsprechen. Die Minimierung des Rayleigh-Quotienten c†Hc/c†Sc\mathbf{c}^\dagger\mathcal{H}\mathbf{c}/\mathbf{c}^\dagger\mathcal{S}\mathbf{c} entspricht daher dem Erlernen des Filters, der das Gewicht des angeregten Zustands der Referenz am besten unterdrückt. Ein größeres Krylov- rr s verleiht dem Filter mehr Freiheitsgrade und einen schärferen Peak bei der Grundzustandsenergie.

Der folgende Helper wertet diesen trainierten Filter auf einer Energieachse aus; anschließend wenden wir ihn auf die oben ermittelte Referenzverteilung an.

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

Stellen Sie sich den Filter und seine Flexibilität vor

Zunächst zeigen wir den trainierten Filter bei der oben verwendeten vollen Krylov-Dimension und verfolgen dann, wie er mit zunehmender Dimension rr an Schärfe gewinnt.

Die Balken zeigen die Referenz-Spektralgewichte pmp_m (vorher) und die gefilterten Gewichte pm∣fKQD(Em)∣2p_m|f_{\rm KQD}(E_m)|^2 (nachher) zusammen mit der gelernten Filterintensität ∣fKQD(E)∣2|f_{\rm KQD}(E)|^2 auf einer kontinuierlichen Energieachse. Der Filter konzentriert das Gewicht auf die niedrigste Energie des Unterraums mit Einzelanregung – dieselbe Energie, zu der die KQD-Schätzung im Beispiel mit kleinem Maßstab konvergierte. Beachten Sie, dass es sich hierbei um den Grundzustand innerhalb des Sektors mit einer einzigen Anregung handelt, der für diesen anregungserhaltenden Referenzzustand relevant ist, und nicht um den globalen Grundzustand.

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

Erhöhung der Krylov-Dimension: Flexibilität der gelernten Funktion

Zur Erinnerung: Der trainierte Filter ist ein trigonometrisches Polynom in der Energie mit Koeffizienten der Form 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*}

Die Krylov-Dimension rr entspricht genau der Anzahl der freien Koeffizienten und bestimmt somit die Flexibilität der Funktion. Ein kleiner „ rr “ kann nur einen breiten, sanft variierenden Filter erzeugen, der Gewicht in tiefer liegende angeregte Zustände abfließen lässt; wenn die „ rr “ zunimmt, kann der Filter einen schmaleren Peak bei der Zielenergie bilden und das verbleibende Gewicht der angeregten Zustände stärker unterdrücken. Dies ist das spektrale Filter-Äquivalent der im Beispiel mit kleinen Skalen beobachteten Energiekonvergenz: Mit zunehmendem Wert von rr kollabiert die gefilterte Verteilung auf den Grundzustand des Unterraums, und die geschätzte Energie nähert sich diesem Wert an.

Wir verwenden die oben bereits geschätzten Projektionsmatrizen erneut und lösen einfach das GEVP bei jedem führenden „ r×rr\times r “-Block, um anschließend den entsprechenden Filter zu berechnen und grafisch darzustellen.

# 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

Nächste Schritte

Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:


Referenzen

[1] E. N. Epperly, L. Lin und Y. Nakatsukasa, Eine Theorie zur Diagonalisierung von Quantenunterräumen, SIAM Journal on Matrix Analysis and Applications 43, 1263–1290 (2022).

[2] N. Yoshioka, M., Amico, W., Kirby u. a., Diagonalisierung großer Vielteilchen-Hamilton-Operatoren auf einem Quantenprozessor, arXiv:2407.14431 (2024).

[3] R. M. Parrish und P. L. McMahon, Diagonalisierung von Quantenfiltern: Quanten-Eigenwertzerlegung ohne vollständige Quantphasenschätzung, Physical Review Letters 122, 230401 (2019).

[4] G. Lee, S. Choi, J. Huh und AF Izmaylov, Effiziente Strategien zur Reduzierung des Abtastfehlers bei der Quanten-Krylov-Unterraum-Diagonalisierung, Digital Discovery 4, 954-969 (2025).

[5] G. Lee, M. Kang, J. Hong, S. Fomichev und J. Hm, „Filtered Quantum Phase Estimation“, arXiv:2510.04294 (2025).

War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.