Diagonalizzazione quantistica di Krylov degli hamiltoniani reticolari
Stima della durata: 70 minuti su un processore Heron o Nighthawk (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)
Risultati di apprendimento
- Come interpretare la diagonalizzazione quantistica di Krylov (KQD) come apprendimento di una funzione hamiltoniana finita che funge da filtro spettrale.
- Come costruire le matrici dell'Hamiltoniano proiettato e di sovrapposizione mediante misurazioni estese con test di scambio.
- Come risolvere il problema degli autovalori generalizzato (GEVP) che ne deriva e ricavare una stima dell'energia dello stato fondamentale per un hamiltoniano reticolare.
Prerequisiti
- Qiskit primitives
- Stima dell'energia dello stato fondamentale della catena di Heisenberg con VQE
- Corso sugli algoritmi di diagonalizzazione quantistica
Sfondo
Questo tutorial illustra come implementare l'algoritmo di diagonalizzazione quantistica di Krylov (KQD) nell'ambito dei pattern di Qiskit. Per prima cosa imparerete la teoria alla base dell'algoritmo e poi assisterete a una dimostrazione della sua esecuzione su una QPU.
La stima delle proprietà a bassa energia degli hamiltoniani a molti corpi è un compito fondamentale nella simulazione quantistica. Ad esempio, le energie dello stato fondamentale e le eccitazioni a bassa energia sono direttamente correlate alla stabilità chimica, all’ordinamento magnetico, alle transizioni di fase quantistiche e alla risposta dei materiali. Su un computer classico, la dimensione dello spazio di Hilbert cresce in modo esponenziale con il numero di orbitali o di spin, per cui la diagonalizzazione diretta diventa ben presto impraticabile.
Esistono diversi approcci basati sull'informatica quantistica per affrontare questo problema. I metodi variazionali a breve termine, come il Variational Quantum Eigensolver (VQE), utilizzano circuiti parametrizzati relativamente poco profondi, ma richiedono un ciclo di ottimizzazione classica non lineare con numerose valutazioni del circuito quantistico. D'altra parte, la stima di fase quantistica (QPE) offre un percorso più diretto verso la stima degli autovalori con garanzie rigorose, ma la QPE standard richiede circuiti coerenti di lunga durata ed è adatta principalmente ai computer quantistici tolleranti ai guasti. Il KQD si colloca a metà strada tra questi due approcci: utilizza l’evoluzione hamiltoniana in tempo reale, come negli algoritmi basati sulla stima di fase, ma sostituisce la stima completa di fase con un problema compatto degli autovalori proiettati che può essere risolto in modo classico.
Si consideri un hamiltoniano a qubit di tipo “ ” e uno stato di riferimento . Il metodo KQD costruisce un sottospazio di Krylov a partire dagli stati evoluti in tempo reale,
dove è la dimensione di Krylov e è il passo temporale. Ogni stato nel sottospazio di Krylov è quindi rappresentato come una combinazione lineare di questi stati di base,
dove il denominatore normalizza lo stato.
Con semplici operazioni algebriche, possiamo vedere che l’energia corrispondente si esprime come quoziente di Rayleigh,
In questo caso, le matrici e ,
definire le matrici di sovrapposizione e dell’Hamiltoniano previste. I loro valori vengono stimati mediante misurazioni sui circuiti quantistici.
Il nostro obiettivo è trovare il coefficiente che dia il minimo :
In base al teorema di Rayleigh-Ritz, questa minimizzazione equivale a risolvere il problema degli autovalori generalizzato (GEVP),
Si noti che la dimensione può essere sufficientemente piccola da consentire a un computer classico di risolvere il GEVP.
Si tratta dello stesso principio variazionale utilizzato nella diagonalizzazione classica dei sottospazi, ma in questo caso gli stati di base sono generati dall’evoluzione temporale quantistica. Rispetto al VQE, il KQD richiede solitamente circuiti più complessi poiché si basa sull’evoluzione in tempo reale. In cambio, il KQD evita l’ottimizzazione non lineare dei parametri e l’esecuzione iterativa su hardware quantistico, e mostra un miglioramento sistematico man mano che il sottospazio proiettato viene ampliato. L'algoritmo è stato testato su larga scala su hardware quantistico esistente [2] e le sue prestazioni possono essere analizzate con garanzie dimostrabili [1].
Requisiti
Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:
- Qiskit SDK v2.3 o versioni successive con supporto alla visualizzazione
- Qiskit Runtime v0.22 o versioni successive (
pip install qiskit-ibm-runtime) - SciPy (
pip install scipy) - Matplotlib (
pip install matplotlib) - Panda (
pip install pandas)
L'esecuzione su hardware richiede qiskit-ibm-runtime l'accesso a un account IBM Quantum®.
Configura
La cella di configurazione importa i moduli necessari e definisce le funzioni di supporto per il flusso di lavoro:
- costruire l'Hamiltoniano di Heisenberg;
- risolvere il GEVP con soglia;
- valutare il filtro di Krylov appreso;
- convertire i valori del filtro in pesi spettrali;
- tracciare le distribuzioni di energia di riferimento e filtrate.
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))Nella prima parte di questo tutorial illustriamo il metodo KQD utilizzando un simulatore a vettore di stato locale. Successivamente, utilizzeremo un backend quantistico reale per affrontare un problema su scala industriale.
Definiamo inoltre un backend fittizio per illustrare la transpilazione specifica del backend e analizzare il circuito risultante.
try:
service = QiskitRuntimeService()
except Exception:
QiskitRuntimeService.save_account(
token="<api_token>", instance="<instance>", overwrite=True
)
service = QiskitRuntimeService()
backend = FakeMarrakesh()Esempio di simulatore su piccola scala
Fase 1: mappare gli input classici su un problema quantistico
Stato hamiltoniano e stato di riferimento
In questo esempio viene utilizzata una catena di Heisenberg a confini aperti da 12 qubit ( ),
con uno stato di prodotto a eccitazione singola
come stato di riferimento. Poiché l'Hamiltoniano di Heisenberg definito sopra conserva il numero totale di eccitazioni, lo stato di riferimento rimane nel sottospazio a singola eccitazione, la cui dimensione cresce solo linearmente con il numero di qubit. Possiamo quindi calcolare in modo efficiente l'energia esatta dello stato fondamentale, diagonalizzando l'Hamiltoniano limitato a quel sottospazio, e utilizzarla esclusivamente come parametro di riferimento diagnostico per la stima KQD. Il flusso di lavoro KQD stima gli elementi della matrice proiettata utilizzando l’ Qiskit primitives e e risolve il problema proiettato che ne deriva con il metodo classico.
# 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)
Impostare i parametri dell'algoritmo
Sulla base dei limiti superiori della norma dell'Hamiltoniano, il rif. [1] suggerisce in modo euristico che il passo temporale sia pari a . Poiché la norma spettrale è difficile da calcolare, utilizziamo invece il suo limite superiore:
Abbiamo impostato la dimensione di Krylov a e il numero di passi di Trotter per ogni passo temporale a : uno spazio di Krylov sufficientemente ampio da risolvere lo spettro delle frequenze basse, mantenendo al contempo un circuito di profondità ( ) gestibile, e un numero di passi di Trotter sufficiente a mantenere basso l'errore di discretizzazione in quel circuito di profondità.
dt = np.pi / (3 * (num_qubits - 1))
print("dt in Krylov basis: ", dt)
krylov_dim = 10
num_trotter_steps = 5Output:
dt in Krylov basis: 0.09519977738150888
Costruire un circuito
In questa sezione, costruiamo i circuiti per stimare gli elementi della matrice e . Poiché tutte le potenze di commutano, si ha che
Tali matrici, i cui elementi dipendono in questo modo dalle differenze tra gli indici, sono dette matrici di Toeplitz e possono essere ricostruite a partire dagli elementi della prima riga indicizzati da .
In questa sede presentiamo il circuito, denominato “extended-swap-test”, che prepara
dove .
Stato di riferimento
Prepariamo lo stato di riferimento .
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:
Evoluzione temporale
Calcolare l'operatore di evoluzione temporale generato dall'Hamiltoniano, approssimato mediante una semplice Lie-Trotterizzazione.
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:
Circuito di prova dello swap esteso [3]
Il circuito prepara innanzitutto lo stato di riferimento nel registro di sistema mentre l'ancilla rimane in modalità " ":
Quindi, applicando una porta di Hadamard all'ancilla si ottiene una sovrapposizione coerente dei due rami:
Infine, si applica il gate di evoluzione temporale controllata:
solo quando l'ancilla si trova nel ramo " ". Pertanto:
Nel blocco di codice seguente, implementiamo:
che verrà assegnato come per nella fase di esecuzione.
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:
Osservabili
Per qualsiasi sistema ermitiano con osservabili , in questa sede definiamo gli osservabili da calcolare come segue:
Questo perché riporta l'elemento di sovrapposizione , mentre riporta l'elemento hamiltoniano .
Utilizzando l' o, si ottiene
Allo stesso modo, utilizzando ,
Pertanto, abbiamo
Infine, per ogni stato , dobbiamo misurare:
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])
Per la stima degli elementi della matrice di , il numero di termini di Pauli è molto maggiore rispetto a quello degli elementi della matrice di .
Ora possiamo ridurre il numero dei termini dell'Hamiltoniano misurati ricorrendo a una tecnica di spostamento [4]. Abbiamo scomposto l'Hamiltoniano come segue:
dove è scelto in modo tale che lo stato di riferimento sia il suo stato proprio,
Quindi,
In questa sezione,
è l'elemento di matrice dell'Hamiltoniano spostato. Pertanto, è sufficiente misurare e . Il contributo proveniente da viene ricostruito in modo classico utilizzando l'elemento della matrice di sovrapposizione già misurato .
In questo esempio, una scelta naturale è la parte diagonale dell'Hamiltoniano di Heisenberg,
Poiché lo stato di riferimento è uno stato di base computazionale, esso è uno stato proprio di ogni termine di tipo “ ”.
Tuttavia, una scelta più vantaggiosa consiste nell’includere non solo i termini diagonali , ma anche i termini che annichiliscono lo stato di riferimento.
Per ogni coppia di vicini, l’operatore soddisfa
e
Pertanto, il termine contribuisce solo quando i due qubit adiacenti presentano occupazioni diverse nella stringa di bit di riferimento. Se i due qubit sono entrambi in stato “ ” o entrambi in stato “ ”, il termine annichilisce lo stato di riferimento e può quindi essere eliminato.
Sia , dove . Pertanto, possiamo scegliere
Questo operatore soddisfa ancora
poiché i termini agiscono in diagonale su , mentre i termini spostati danno zero. L'autovalore corrispondente è quindi determinato esclusivamente dai termini di tipo " ",
Con questa scelta, l'Hamiltoniano spostato diventa:
Di conseguenza, è necessario misurare solo i bordi con occupazioni diverse nello stato di riferimento. Tutti i termini di e tutti i termini inattivi di vengono ricostruiti tramite il contributo di sovrapposizione , oppure, per definizione, forniscono un contributo pari a zero.
In questo modo si ottiene un osservabile più piccolo rispetto a quando si sposta solo la parte diagonale. In particolare, per uno stato di riferimento a base computazionale con un’eccitazione localizzata, solo i bordi adiacenti all’eccitazione rimangono nell’ e. Pertanto, il numero di termini di Pauli in e può essere sostanzialmente ridotto, mentre l’elemento di matrice ricostruito
rimane esattamente lo stesso.
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
Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico
Ora trasformiamo il circuito astratto del test di swap esteso in un modello orientato all'hardware. Prima di ciò, ottimizziamo ulteriormente il circuito, partendo dal livello astratto.
Confronto tra l'ordinamento dei termini dell'Hamiltoniano
In primo luogo, mettiamo a confronto diversi ordinamenti dei termini di Pauli nell’Hamiltoniano di Heisenberg ai fini della simulazione hamiltoniana. L'Hamiltoniano in sé rimane invariato, ma l'ordinamento influisce sul modo in cui viene generato il circuito in forma di prodotto e sul grado di parallelizzazione della struttura all'interno del circuito. Ad esempio, l'ordinamento ingenuo elenca prima tutti i termini " ", poi tutti i termini " " e infine tutti i termini " ". In questo modo, i bordi adiacenti, come e , vengono posizionati uno accanto all'altro, impedendone l'esecuzione in parallelo. L'ordinamento "pari-poi-dispari" visita prima i bordi pari disgiunti, seguiti dai bordi dispari, il che mette in evidenza strati paralleli di due qubit. L'ordinamento per gruppi di spigoli pari-dispari fa un ulteriore passo avanti: per ogni spigolo, mantiene raggruppati i termini locali , e , pur continuando a visitare gli spigoli pari prima di quelli dispari. Prevediamo che gli ordinamenti “pari-poi-dispari” e “pari-dispari” raggruppati per bordo riducano la profondità del circuito grazie all’esposizione di strati paralleli di due qubit, e che l’ordinamento raggruppato per bordo riduca ulteriormente l’errore di Trotter, poiché l’interazione locale tra due qubit sullo stesso bordo viene gestita come un blocco compatto.
L'ordinamento hamiltoniano influisce anche sull'errore di Trotter. Se disponiamo i termini non commutativi uno accanto all’altro, le transizioni di base si verificano più spesso, il che induce un maggiore errore di Trotter. Raggruppando i termini che richiedono la stessa trasformazione delle basi di Pauli, è possibile evitare cambiamenti di base superflui.
In questo caso, il confronto utilizza il tempo di evoluzione massimo che compare nelle stime di Krylov della prima riga, , utilizzando la stessa condizione di transpilazione.
Per misurare l’errore di Trotter, utilizziamo l’infedeltà di processo tra il circuito trotterizzato e l’evoluzione esatta dell’hamiltoniano,
dove è la dimensione dello spazio di Hilbert.
Questa procedura diagnostica utilizza matrici dense, pertanto è adatta a questo piccolo esempio da 12 qubit, ma non è concepita come subroutine scalabile.
# 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
Inizialmente manteniamo la regola di sintesi fissata a un passo di Trotter di primo ordine e variamo solo l'ordine dei termini di Pauli. L'obiettivo di questo confronto è principalmente quello di verificare di quanto sia possibile ridurre la profondità del circuito e il costo per due qubit esponendo al transpiler i bordi disgiunti dei vicini più prossimi.
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']
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']
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']
Si osserva che sia l’ordinamento “pari-dopo-dispari” che quello “pari-dispari” con raggruppamento per bordi riducono entrambi la profondità da 15 a 6 reps=1, esponendo strati paralleli di due qubit, mentre il numero di gate a due qubit rimane invariato in tutti e tre gli ordinamenti.
Tuttavia, dato che in tutti e tre gli ordinamenti l’infedeltà è vicina a 1, variamo il numero di ripetizioni di Trotter per distinguere più chiaramente gli ordinamenti.
Man mano che il numero di ripetizioni aumenta, l’infedeltà dell’ordinamento even-odd edge-grouped diminuisce più rapidamente rispetto alle altre due, raggiungendo 0.045 a a reps=8 rispetto a 0.075 per l’ordinamento ingenuo e quello “prima pari, poi dispari”.
Confronto tra la sintesi di prodotti e la sintesi di formule
Successivamente, analizzeremo diverse impostazioni avanzate della "Trotterizzazione", passando dall'ordinamento hamiltoniano fisso all'ordinamento con raggruppamento dei bordi in pari-dispari. Prendiamo in esame il sistema di Lie-Trotter di primo ordine, quello di Suzuki-Trotter di secondo ordine e quello di Suzuki-Trotter di quarto ordine.
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 = NoneOutput:
Il metodo Lie-Trotter fornisce il circuito meno profondo ma presenta l'errore maggiore, mentre il metodo Suzuki-Trotter di quarto ordine è più preciso ma aumenta la profondità del circuito. Per il resto del tutorial, scegliamo la formula di Suzuki-Trotter di secondo ordine perché fornisce un circuito di profondità ridotta, riducendo al contempo in modo sostanziale l'errore di Trotter rispetto alla formula di primo ordine.
Rimuovere il gate di evoluzione temporale controllata
Nel test di scambio esteso, per controllare l'evoluzione temporale con un singolo qubit ancilla è necessario che l'ancilla controlli numerosi gate in tutto il sistema. Ciò può comportare un notevole sovraccarico di routing e, nel peggiore dei casi, richiede di fatto una connettività "tutti-a-uno". Per evitare ciò, è possibile ottenere un’ulteriore ottimizzazione sostituendo il gate di evoluzione temporale controllato con una versione senza controllo, sfruttando la simmetria dell’Hamiltoniano. Osserviamo il seguente circuito.
Qui, illustra lo stato di riferimento, .
Anziché preparare prima e poi applicare solo sul ramo , il circuito prepara direttamente i due rami come
Dove
Il circuito applica innanzitutto una porta di Hadamard all’ancilla e prepara lo stato di riferimento solo sul ramo “ ”:
Quindi si applica l'operatore di evoluzione temporale non controllato a entrambi i rami:
Poiché l'Hamiltoniano conserva il numero di eccitazione, si può notare che è uno dei suoi stati propri e, pertanto, l'operatore di evoluzione accumula solo una fase sotto l'Hamiltoniano:
Pertanto:
Successivamente, " " viene applicato solo al ramo " ".
A questo punto, i due rami presentano una fase relativa in più. Per eliminarlo, applichiamo il gate di fase ancilla
Questo trasforma lo stato come
Pertanto, fino a una fase globale irrilevante, abbiamo infine preparato
Nel prossimo blocco di codice implementiamo questo circuito senza elementi di controllo.
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
Trasposizione
A questo punto, trasponiamo i circuiti con controllo e quelli senza controllo in modo che siano eseguibili sull'hardware. Confrontiamo i risultati dei circuiti transpilati.
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:
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>
})
Passaggio 3: eseguire utilizzando Qiskit primitives
Il passo successivo consiste nell’applicare lo stesso circuito parametrizzato a diversi valori di . Per ogni , si stimano quattro valori attesi: , , e . Questi quattro numeri vengono poi combinati negli elementi complessi della prima riga e .
In questo caso, e possono essere calcolati in modo classico poiché è sparso, quindi tralasciamo il caso .
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']
Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato
Dopo aver stimato le matrici proiettate, regolarizziamo e risolviamo il GEVP
Il più piccolo autovalore generalizzato fornisce la stima KQD dell'energia dello stato fondamentale.
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
Ora risolviamo il problema degli autovalori generalizzato utilizzando le matrici ricostruite a partire dalle stime del circuito. In un calcolo del vettore di stato ideale con evoluzione esatta in tempo reale, ciò dovrebbe riprodurre il risultato proiettato esatto. In pratica, le deviazioni possono derivare dalla trotterizzazione, dall’errore di campionamento e dall’instabilità numerica della matrice di sovrapposizione.
Osserviamo come l'energia converga all'aumentare della dimensione del sottospazio di Krylov.
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:
Esempio di hardware su larga scala
Nella sezione precedente è stato utilizzato un modello a 12 qubit in modo che la simulazione del vettore di stato potesse essere impiegata come strumento diagnostico. Ora estendiamo lo stesso flusso di lavoro KQD a una catena di Heisenberg da 30 qubit e prepariamo il carico di lavoro per l'esecuzione su un hardware d IBM Quantum.
I passaggi da 1 a 4 raggruppati in un unico blocco di codice
A questo punto riuniamo tutti questi dettagli in un unico flusso di lavoro su scala più ampia, che viene poi eseguito sul nostro hardware quantistico reale. In questa sezione applichiamo impostazioni realistiche di mitigazione degli errori per migliorare l'affidabilità dei risultati. Poiché gli elementi della matrice corrispondenti a diversi valori di possono essere calcolati in parallelo, utilizziamo la modalità Batch per eseguirli in modo efficiente.
# -------------------------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
Appendice: Il punto di vista della funzione hamiltoniana (filtro spettrale)
Il flusso di lavoro principale ha illustrato l'applicazione pratica del KQD: costruzione di una base di Krylov a partire dagli stati evoluti in tempo reale, stima delle matrici proiettate e e risoluzione del GEVP. La presente appendice riprende lo stesso calcolo da una prospettiva complementare che spiega il funzionamento del KQD: il punto di vista della funzione hamiltoniana, o del filtro spettrale [3], [5]. Riutilizza il modello a 12 qubit, il passo temporale e la soluzione di Krylov già ottenuta in precedenza; non è necessaria alcuna nuova esecuzione del circuito.
Lo stato di riferimento come distribuzione di energia
Supponiamo che l'Hamiltoniano abbia la seguente decomposizione in autovalori
con stati propri di energia . Qualsiasi stato di riferimento può essere sviluppato in questa base propria,
pertanto presenta un peso spettrale per ciascuna energia . L'energia di riferimento è la media di questa distribuzione, .
La decomposizione in autovalori di un Hamiltoniano generale di tipo -qubit è esponenzialmente onerosa, pertanto questa rappresentazione ha solo scopo diagnostico e non fa parte dell'algoritmo. In questo caso, tuttavia, possiamo calcolarlo in modo efficiente per lo stesso problema di “ ”: l’Hamiltoniano di Heisenberg conserva il numero totale di eccitazioni, e la distribuzione di riferimento presenta una singola eccitazione, quindi il suo intero contenuto spettrale risiede nel sottospazio a singola eccitazione, la cui dimensione cresce solo linearmente con . Riutilizziamo quindi il blocco esatto a singola eccitazione (già utilizzato in precedenza come benchmark) e ricaviamo la distribuzione di riferimento all’interno di quel sottospazio.
# 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 apprende un filtro che rimodella questa distribuzione
Una funzione hamiltoniana viene definita tramite il calcolo spettrale,
o, in altre parole, una somma ponderata di proiettori propri. Applicandolo al riferimento, si rimodella ciascuna ampiezza spettrale, :
Se l' e presentasse un picco molto pronunciato all'energia minima ( e in caso contrario), allora fungerebbe da proiettore dello stato fondamentale e l'uscita normalizzata sarebbe (vicina allo) stato fondamentale. Pertanto, un buon filtro spettrale passa-basso in termini di energia è esattamente ciò che ci serve.
La KQD non prescrive in anticipo l' . Al contrario, espande il filtro sulla base dell'evoluzione in tempo reale,
una funzione trigonometrica dell'energia i cui coefficienti sono proprio l'autovettore GEVP calcolato in precedenza. Ridurre al minimo il quoziente di Rayleigh equivale quindi a individuare il filtro che sopprime al meglio il peso dello stato eccitato del segnale di riferimento. Una dimensione di Krylov maggiore conferisce al filtro un maggior numero di gradi di libertà e un picco più nitido all'energia dello stato fondamentale.
L'helper riportato di seguito valuta questo filtro appreso su un asse di energia; lo applichiamo quindi alla distribuzione di riferimento ottenuta in precedenza.
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
Visualizza il filtro e la sua flessibilità
Mostriamo innanzitutto il filtro appreso alla dimensione di Krylov completa utilizzata in precedenza, quindi osserviamo come la sua precisione aumenti all’aumentare della dimensione .
Le barre mostrano i pesi spettrali di riferimento (prima) e i pesi filtrati (dopo), insieme all'intensità del filtro appreso su un asse energetico continuo. Il filtro concentra il peso sull'energia più bassa del sottospazio a eccitazione singola — la stessa energia verso cui convergeva la stima KQD nell'esempio su piccola scala. Si noti che si tratta dello stato fondamentale all’interno del settore a singola eccitazione, che è l’obiettivo rilevante per questo stato di riferimento che conserva l’eccitazione, e non dello stato fondamentale globale.
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:
Aumento della dimensione di Krylov: flessibilità della funzione appresa
Ricordiamo che il filtro appreso è un polinomio trigonometrico in energia con coefficienti di tip ,
La dimensione di Krylov corrisponde esattamente al numero di coefficienti liberi, quindi determina la flessibilità della funzione. Un filtro di tipo “ ” di piccole dimensioni può produrre solo un filtro ampio e con variazioni lievi, che disperde parte del peso verso stati eccitati di energia inferiore; quando l’ e aumenta, il filtro può formare un picco più stretto all’energia desiderata e sopprimere in modo più deciso il peso residuo degli stati eccitati. Si tratta della controparte, in termini di filtro spettrale, della convergenza energetica osservata nell’esempio su piccola scala: all’aumentare dell’ , la distribuzione filtrata converge verso lo stato fondamentale del sottospazio e l’energia stimata diminuisce avvicinandosi ad esso.
Riutilizziamo le matrici proiettate già stimate in precedenza e risolviamo semplicemente il GEVP in ciascun blocco principale , quindi calcoliamo e tracciamo il filtro corrispondente.
# 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
Passi successivi
Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti materiali:
Riferimenti
[1] E. N. Epperly, L. Lin e Y. Nakatsukasa, Una teoria della diagonalizzazione dei sottospazi quantistici, SIAM Journal on Matrix Analysis and Applications 43, 1263-1290 (2022).
[2] N. Yoshioka, M. Amico, W. Kirby, et al., Diagonalizzazione di hamiltoniani a molti corpi di grandi dimensioni su un processore quantistico, arXiv:2407.14431 (2024).
[3] R. M. Parrish e P. L. McMahon, “Diagonalizzazione dei filtri quantistici: decomposizione in autovalori quantistici senza stima completa della fase quantistica ”, Physical Review Letters 122, 230401 (2019).
[4] G. Lee, S. Choi, J. Huh e AF Izmaylov, Strategie efficienti per ridurre l'errore di campionamento nella diagonalizzazione dello spazio vettoriale di Krylov quantistico, Digital Discovery 4, 954-969 (2025).
[5] G. Lee, M. Kang, J. Hong, S. Fomichev e J. Eh, “Filtered Quantum Phase Estimation”, arXiv:2510.04294 (2025).