Diagonalisation quantique de Krylov pour les hamiltoniens de réseau
Estimation d'utilisation : 20 minutes sur un Heron r2 (REMARQUE : ceci est une estimation uniquement. Ton temps d'exécution peut varier.)
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime scipy sympy
# This cell is hidden from users – it disables some lint rules
# ruff: noqa: E402 E722 F601
Contexte
Ce tutoriel montre comment implémenter l'algorithme de diagonalisation quantique de Krylov (KQD) dans le cadre des modèles Qiskit. Tu apprendras d'abord la théorie sous-jacente à l'algorithme, puis tu verras une démonstration de son exécution sur un QPU.
Dans de nombreuses disciplines, nous cherchons à connaître les propriétés de l'état fondamental des systèmes quantiques. Parmi les exemples, on trouve la compréhension de la nature fondamentale des particules et des forces, la prédiction et la compréhension du comportement de matériaux complexes, ainsi que la compréhension des interactions et réactions biochimiques. En raison de la croissance exponentielle de l'espace de Hilbert et des corrélations qui apparaissent dans les systèmes intriqués, les algorithmes classiques peinent à résoudre ce problème pour des systèmes quantiques de taille croissante. À une extrémité du spectre, l'approche existante qui tire parti du matériel quantique se concentre sur les méthodes quantiques variationnelles (par exemple, le solveur quantique variationnel des valeurs propres). Ces techniques font face à des défis avec les dispositifs actuels en raison du grand nombre d'appels de fonction requis dans le processus d'optimisation, ce qui ajoute une surcharge importante en ressources une fois que des techniques avancées d'atténuation des erreurs sont introduites, limitant ainsi leur efficacité aux petits systèmes. À l'autre extrémité du spectre, il existe des méthodes quantiques tolérantes aux fautes avec des garanties de performance (par exemple, l'estimation de phase quantique), qui nécessitent des circuits profonds ne pouvant être exécutés que sur un dispositif tolérant aux fautes. Pour ces raisons, nous introduisons ici un algorithme quantique basé sur les méthodes de sous-espace (tel que décrit dans cet article de synthèse), l'algorithme de diagonalisation quantique de Krylov (KQD). Cet algorithme fonctionne bien à grande échelle [1] sur le matériel quantique existant, partage des garanties de performance similaires à celles de l'estimation de phase, est compatible avec les techniques avancées d'atténuation des erreurs et pourrait fournir des résultats classiquement inaccessibles.
Prérequis
Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants :
- Qiskit SDK v2.0 ou version ultérieure, avec le support de la visualisation
- Qiskit Runtime v0.22 ou version ultérieure (
pip install qiskit-ibm-runtime)
Configuration
import numpy as np
import scipy as sp
import matplotlib.pylab as plt
from typing import Union, List
import itertools as it
import copy
from sympy import Matrix
import warnings
warnings.filterwarnings("ignore")
from qiskit.quantum_info import SparsePauliOp, Pauli, StabilizerState
from qiskit.circuit import Parameter, IfElseOp
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.synthesis import LieTrotter
from qiskit.transpiler import Target, CouplingMap
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
EstimatorV2 as Estimator,
)
def solve_regularized_gen_eig(
h: np.ndarray,
s: np.ndarray,
threshold: float,
k: int = 1,
return_dimn: bool = False,
) -> Union[float, List[float]]:
"""
Method for solving the generalized eigenvalue problem with regularization
Args:
h (numpy.ndarray):
The effective representation of the matrix in the Krylov subspace
s (numpy.ndarray):
The matrix of overlaps between vectors of the Krylov subspace
threshold (float):
Cut-off value for the eigenvalue of s
k (int):
Number of eigenvalues to return
return_dimn (bool):
Whether to return the size of the regularized subspace
Returns:
lowest k-eigenvalue(s) that are the solution of the
regularized generalized eigenvalue problem
"""
s_vals, s_vecs = sp.linalg.eigh(s)
s_vecs = s_vecs.T
good_vecs = np.array(
[vec for val, vec in zip(s_vals, s_vecs) if val > threshold]
)
h_reg = good_vecs.conj() @ h @ good_vecs.T
s_reg = good_vecs.conj() @ s @ good_vecs.T
if k == 1:
if return_dimn:
return sp.linalg.eigh(h_reg, s_reg)[0][0], len(good_vecs)
else:
return sp.linalg.eigh(h_reg, s_reg)[0][0]
else:
if return_dimn:
return sp.linalg.eigh(h_reg, s_reg)[0][:k], len(good_vecs)
else:
return sp.linalg.eigh(h_reg, s_reg)[0][:k]
def single_particle_gs(H_op, n_qubits):
"""
Find the ground state of the single particle(excitation) sector
"""
H_x = []
for p, coeff in H_op.to_list():
H_x.append(set([i for i, v in enumerate(Pauli(p).x) if v]))
H_z = []
for p, coeff in H_op.to_list():
H_z.append(set([i for i, v in enumerate(Pauli(p).z) if v]))
H_c = H_op.coeffs
print("n_sys_qubits", n_qubits)
n_exc = 1
sub_dimn = int(sp.special.comb(n_qubits + 1, n_exc))
print("n_exc", n_exc, ", subspace dimension", sub_dimn)
few_particle_H = np.zeros((sub_dimn, sub_dimn), dtype=complex)
# list all of the possible sets of n_exc indices of 1s in
# n_exc-particle states
sparse_vecs = [
set(vec) for vec in it.combinations(range(n_qubits + 1), r=n_exc)
]
m = 0
for i, i_set in enumerate(sparse_vecs):
for j, j_set in enumerate(sparse_vecs):
m += 1
if len(i_set.symmetric_difference(j_set)) <= 2:
for p_x, p_z, coeff in zip(H_x, H_z, H_c):
if i_set.symmetric_difference(j_set) == p_x:
sgn = ((-1j) ** len(p_x.intersection(p_z))) * (
(-1) ** len(i_set.intersection(p_z))
)
else:
sgn = 0
few_particle_H[i, j] += sgn * coeff
gs_en = min(np.linalg.eigvalsh(few_particle_H))
print("single particle ground state energy: ", gs_en)
return gs_en
Étape 1 : Transposer les entrées classiques en un problème quantique
L'espace de Krylov
L'espace de Krylov d'ordre est l'espace engendré par les vecteurs obtenus en multipliant des puissances croissantes d'une matrice , jusqu'à , avec un vecteur de référence .
Si la matrice est le hamiltonien , nous désignerons l'espace correspondant comme l'espace de Krylov en puissances . Dans le cas où est l'opérateur d'évolution temporelle généré par le hamiltonien , nous désignerons l'espace comme l'espace de Krylov unitaire . Le sous-espace de Krylov en puissances que nous utilisons classiquement ne peut pas être généré directement sur un ordinateur quantique car n'est pas un opérateur unitaire. En revanche, nous pouvons utiliser l'opérateur d'évolution temporelle qui offre des garanties de convergence similaires à celles de la méthode des puissances. Les puissances de deviennent alors différents pas de temps .
Consulte l'annexe pour une dérivation détaillée montrant comment l'espace de Krylov unitaire permet de représenter avec précision les états propres de basse énergie.
Algorithme de diagonalisation quantique de Krylov
Étant donné un hamiltonien que nous souhaitons diagonaliser, nous considérons d'abord l'espace de Krylov unitaire correspondant . L'objectif est de trouver une représentation compacte du hamiltonien dans , que nous désignerons par . Les éléments de matrice de , la projection du hamiltonien dans l'espace de Krylov, peuvent être calculés en évaluant les valeurs moyennes suivantes
Où sont les vecteurs de l'espace de Krylov unitaire et sont les multiples du pas de temps choisi. Sur un ordinateur quantique, le calcul de chaque élément de matrice peut être effectué avec tout algorithme permettant d'obtenir le recouvrement entre états quantiques. Ce tutoriel se concentre sur le test de Hadamard. Étant donné que a une dimension , le hamiltonien projeté dans le sous-espace aura des dimensions . Avec suffisamment petit (généralement suffit pour obtenir la convergence des estimations des énergies propres), nous pouvons alors facilement diagonaliser le hamiltonien projeté . Cependant, nous ne pouvons pas diagonaliser directement en raison de la non-orthogonalité des vecteurs de l'espace de Krylov. Nous devrons mesurer leurs recouvrements et construire une matrice
Cela nous permet de résoudre le problème aux valeurs propres dans un espace non orthogonal (également appelé problème aux valeurs propres généralisé)
On peut alors obtenir des estimations des valeurs propres et des états propres de en examinant ceux de . Par exemple, l'estimation de l'énergie de l'état fondamental est obtenue en prenant la plus petite valeur propre et l'état fondamental à partir du vecteur propre correspondant . Les coefficients dans déterminent la contribution des différents vecteurs qui engendrent .

La figure montre une représentation en circuit du test de Hadamard modifié, une méthode utilisée pour calculer le recouvrement entre différents états quantiques. Pour chaque élément de matrice , un test de Hadamard entre l'état et est effectué. Ceci est mis en évidence dans la figure par le code couleur des éléments de matrice et les opérations correspondantes , . Ainsi, un ensemble de tests de Hadamard pour toutes les combinaisons possibles de vecteurs de l'espace de Krylov est nécessaire pour calculer tous les éléments de matrice du hamiltonien projeté . Le fil supérieur dans le circuit du test de Hadamard est un qubit ancillaire qui est mesuré soit dans la base X, soit dans la base Y ; sa valeur moyenne détermine la valeur du recouvrement entre les états. Le fil inférieur représente tous les qubits du hamiltonien du système. L'opération prépare le qubit du système dans l'état de manière contrôlée par l'état du qubit ancillaire (de même pour ), et l'opération représente la décomposition de Pauli du hamiltonien du système . Une dérivation plus détaillée des opérations calculées par le test de Hadamard est donnée ci-dessous.
Définition du hamiltonien
Considérons le hamiltonien de Heisenberg pour qubits sur une chaîne linéaire :
# Define problem Hamiltonian.
n_qubits = 30
J = 1 # coupling strength for ZZ interaction
# Define the Hamiltonian:
H_int = [["I"] * n_qubits for _ in range(3 * (n_qubits - 1))]
for i in range(n_qubits - 1):
H_int[i][i] = "Z"
H_int[i][i + 1] = "Z"
for i in range(n_qubits - 1):
H_int[n_qubits - 1 + i][i] = "X"
H_int[n_qubits - 1 + i][i + 1] = "X"
for i in range(n_qubits - 1):
H_int[2 * (n_qubits - 1) + i][i] = "Y"
H_int[2 * (n_qubits - 1) + i][i + 1] = "Y"
H_int = ["".join(term) for term in H_int]
H_tot = [(term, J) if term.count("Z") == 2 else (term, 1) for term in H_int]
# Get operator
H_op = SparsePauliOp.from_list(H_tot)
print(H_tot)
[('ZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIZZIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIZZIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIZZIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIZZIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIZZIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 1), ('XXIIIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IXXIIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIXXIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIXXIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIXXIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIXXIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIXXIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIXXIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIXXIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIXXIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIXXIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIXXIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIXXIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIXXIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIXXIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIXXIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIXXIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIXXIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIXXIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIXXIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIXXIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIXXIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIXXIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIXXIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIXXIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIXXIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIXXII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIIXXI', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIIIXX', 1), ('YYIIIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IYYIIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIYYIIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIYYIIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIYYIIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIYYIIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIYYIIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIYYIIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIYYIIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIYYIIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIYYIIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIYYIIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIYYIIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIYYIIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIYYIIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIYYIIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIYYIIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIYYIIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIYYIIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIYYIIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIYYIIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIYYIIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIYYIIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIYYIIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIYYIIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIYYIII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIYYII', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIIYYI', 1), ('IIIIIIIIIIIIIIIIIIIIIIIIIIIIYY', 1)]
Définition des paramètres de l'algorithme
Nous choisissons heuristiquement une valeur pour le pas de temps dt (basée sur des bornes supérieures de la norme du hamiltonien). La référence [2] a montré qu'un pas de temps suffisamment petit est , et qu'il est préférable, jusqu'à un certain point, de sous-estimer cette valeur plutôt que de la surestimer, car une surestimation peut permettre aux contributions des états de haute énergie de corrompre même l'état optimal dans l'espace de Krylov. En revanche, choisir un trop petit conduit à un moins bon conditionnement du sous-espace de Krylov, puisque les vecteurs de la base de Krylov diffèrent moins d'un pas de temps à l'autre.
# Get Hamiltonian restricted to single-particle states
single_particle_H = np.zeros((n_qubits, n_qubits))
for i in range(n_qubits):
for j in range(i + 1):
for p, coeff in H_op.to_list():
p_x = Pauli(p).x
p_z = Pauli(p).z
if all(
p_x[k] == ((i == k) + (j == k)) % 2 for k in range(n_qubits)
):
sgn = (
(-1j) ** sum(p_z[k] and p_x[k] for k in range(n_qubits))
) * ((-1) ** p_z[i])
else:
sgn = 0
single_particle_H[i, j] += sgn * coeff
for i in range(n_qubits):
for j in range(i + 1, n_qubits):
single_particle_H[i, j] = np.conj(single_particle_H[j, i])
# Set dt according to spectral norm
dt = np.pi / np.linalg.norm(single_particle_H, ord=2)
dt
np.float64(0.10833078115826875)
Nous définissons ensuite les autres paramètres de l'algorithme. Dans le cadre de ce tutoriel, nous nous limiterons à utiliser un espace de Krylov de seulement cinq dimensions, ce qui est assez restrictif.
# Set parameters for quantum Krylov algorithm
krylov_dim = 5 # size of Krylov subspace
num_trotter_steps = 6
dt_circ = dt / num_trotter_steps
Préparation de l'état
Choisis un état de référence qui possède un certain recouvrement avec l'état fondamental. Pour ce hamiltonien, nous utilisons un état avec une excitation sur le qubit central comme état de référence.
qc_state_prep = QuantumCircuit(n_qubits)
qc_state_prep.x(int(n_qubits / 2) + 1)
qc_state_prep.draw("mpl", scale=0.5)
Évolution temporelle
Nous pouvons réaliser l'opérateur d'évolution temporelle généré par un hamiltonien donné : via l'approximation de Lie-Trotter.
t = Parameter("t")
## Create the time-evo op circuit
evol_gate = PauliEvolutionGate(
H_op, time=t, synthesis=LieTrotter(reps=num_trotter_steps)
)
qr = QuantumRegister(n_qubits)
qc_evol = QuantumCircuit(qr)
qc_evol.append(evol_gate, qargs=qr)
<qiskit.circuit.instructionset.InstructionSet at 0x11eef9be0>
Test de Hadamard

Où est l'un des termes de la décomposition du hamiltonien et , sont des opérations contrôlées qui préparent les vecteurs , de l'espace de Krylov unitaire, avec . Pour mesurer , on applique d'abord ...
... puis on mesure :
À partir de l'identité . De manière similaire, la mesure de donne
## Create the time-evo op circuit
evol_gate = PauliEvolutionGate(
H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)
)
## Create the time-evo op dagger circuit
evol_gate_d = PauliEvolutionGate(
H_op, time=dt, synthesis=LieTrotter(reps=num_trotter_steps)
)
evol_gate_d = evol_gate_d.inverse()
# Put pieces together
qc_reg = QuantumRegister(n_qubits)
qc_temp = QuantumCircuit(qc_reg)
qc_temp.compose(qc_state_prep, inplace=True)
for _ in range(num_trotter_steps):
qc_temp.append(evol_gate, qargs=qc_reg)
for _ in range(num_trotter_steps):
qc_temp.append(evol_gate_d, qargs=qc_reg)
qc_temp.compose(qc_state_prep.inverse(), inplace=True)
# Create controlled version of the circuit
controlled_U = qc_temp.to_gate().control(1)
# Create hadamard test circuit for real part
qr = QuantumRegister(n_qubits + 1)
qc_real = QuantumCircuit(qr)
qc_real.h(0)
qc_real.append(controlled_U, list(range(n_qubits + 1)))
qc_real.h(0)
print(
"Circuit for calculating the real part of the overlap in S via Hadamard test"
)
qc_real.draw("mpl", fold=-1, scale=0.5)
Circuit for calculating the real part of the overlap in S via Hadamard test
Le circuit du test de Hadamard peut être un circuit profond une fois que nous le décomposons en portes natives (ce qui augmentera encore davantage si nous tenons compte de la topologie du dispositif)
print(
"Number of layers of 2Q operations",
qc_real.decompose(reps=2).depth(lambda x: x[0].num_qubits == 2),
)
Number of layers of 2Q operations 112753
Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique
Test de Hadamard efficace
Nous pouvons optimiser les circuits profonds pour le test de Hadamard que nous avons obtenus en introduisant certaines approximations et en nous appuyant sur certaines hypothèses concernant le Hamiltonien du modèle. Par exemple, considérons le circuit suivant pour le test de Hadamard :

Supposons que nous pouvons calculer classiquement , la valeur propre de sous le Hamiltonien . Cette condition est satisfaite lorsque le Hamiltonien préserve la symétrie U(1). Bien que cela puisse sembler être une hypothèse forte, il existe de nombreux cas où il est raisonnable de supposer qu'il existe un état de vide (qui dans ce cas correspond à l'état ) qui n'est pas affecté par l'action du Hamiltonien. C'est vrai par exemple pour les Hamiltoniens de chimie qui décrivent des molécules stables (où le nombre d'électrons est conservé). Étant donné que la porte prépare l'état de référence souhaité , par exemple, pour préparer l'état HF en chimie, serait un produit de portes NOT mono-qubit, de sorte que la version contrôlée controlled- est simplement un produit de portes CNOT. Alors le circuit ci-dessus implémente l'état suivant avant la mesure :
où nous avons utilisé le déphasage calculable classiquement à la troisième ligne. Les valeurs moyennes sont donc obtenues comme suit
En utilisant ces hypothèses, nous avons pu exprimer les valeurs moyennes des opérateurs d'intérêt avec moins d'opérations contrôlées. En effet, nous n'avons besoin d'implémenter que la préparation d'état contrôlée et non les évolutions temporelles contrôlées. Reformuler notre calcul comme ci-dessus nous permettra de réduire considérablement la profondeur des circuits résultants.
Décomposer l'opérateur d'évolution temporelle avec la décomposition de Trotter
Au lieu d'implémenter exactement l'opérateur d'évolution temporelle, nous pouvons utiliser la décomposition de Trotter pour en implémenter une approximation. Répéter plusieurs fois une décomposition de Trotter d'un certain ordre nous permet de réduire davantage l'erreur introduite par l'approximation. Dans ce qui suit, nous construisons directement l'implémentation de Trotter de la manière la plus efficace pour le graphe d'interaction du Hamiltonien que nous considérons (interactions aux plus proches voisins uniquement). En pratique, nous insérons des rotations de Pauli , , avec un angle paramétré qui correspondent à l'implémentation approchée de . Étant donné la différence de définition entre les rotations de Pauli et l'évolution temporelle que nous essayons d'implémenter, nous devrons utiliser le paramètre pour obtenir une évolution temporelle de . De plus, nous inversons l'ordre des opérations pour un nombre impair de répétitions des étapes de Trotter, ce qui est fonctionnellement équivalent mais permet de synthétiser des opérations adjacentes en une seule unitaire . Cela donne un circuit beaucoup moins profond que celui obtenu en utilisant la fonctionnalité générique PauliEvolutionGate().
t = Parameter("t")
# Create instruction for rotation about XX+YY-ZZ:
Rxyz_circ = QuantumCircuit(2)
Rxyz_circ.rxx(t, 0, 1)
Rxyz_circ.ryy(t, 0, 1)
Rxyz_circ.rzz(t, 0, 1)
Rxyz_instr = Rxyz_circ.to_instruction(label="RXX+YY+ZZ")
interaction_list = [
[[i, i + 1] for i in range(0, n_qubits - 1, 2)],
[[i, i + 1] for i in range(1, n_qubits - 1, 2)],
] # linear chain
qr = QuantumRegister(n_qubits)
trotter_step_circ = QuantumCircuit(qr)
for i, color in enumerate(interaction_list):
for interaction in color:
trotter_step_circ.append(Rxyz_instr, interaction)
if i < len(interaction_list) - 1:
trotter_step_circ.barrier()
reverse_trotter_step_circ = trotter_step_circ.reverse_ops()
qc_evol = QuantumCircuit(qr)
for step in range(num_trotter_steps):
if step % 2 == 0:
qc_evol = qc_evol.compose(trotter_step_circ)
else:
qc_evol = qc_evol.compose(reverse_trotter_step_circ)
qc_evol.decompose().draw("mpl", fold=-1, scale=0.5)

Utiliser un circuit optimisé pour la préparation d'état
control = 0
excitation = int(n_qubits / 2) + 1
controlled_state_prep = QuantumCircuit(n_qubits + 1)
controlled_state_prep.cx(control, excitation)
controlled_state_prep.draw("mpl", fold=-1, scale=0.5)
Circuits modèles pour le calcul des éléments de matrice de et via le test de Hadamard
La seule différence entre les circuits utilisés dans le test de Hadamard réside dans la phase de l'opérateur d'évolution temporelle et les observables mesurées. Nous pouvons donc préparer un circuit modèle qui représente le circuit générique pour le test de Hadamard, avec des emplacements réservés pour les portes qui dépendent de l'opérateur d'évolution temporelle.
# Parameters for the template circuits
parameters = []
for idx in range(1, krylov_dim):
parameters.append(2 * dt_circ * (idx))
# Create modified hadamard test circuit
qr = QuantumRegister(n_qubits + 1)
qc = QuantumCircuit(qr)
qc.h(0)
qc.compose(controlled_state_prep, list(range(n_qubits + 1)), inplace=True)
qc.barrier()
qc.compose(qc_evol, list(range(1, n_qubits + 1)), inplace=True)
qc.barrier()
qc.x(0)
qc.compose(
controlled_state_prep.inverse(), list(range(n_qubits + 1)), inplace=True
)
qc.x(0)
qc.decompose().draw("mpl", fold=-1)

print(
"The optimized circuit has 2Q gates depth: ",
qc.decompose().decompose().depth(lambda x: x[0].num_qubits == 2),
)
The optimized circuit has 2Q gates depth: 74
Nous avons considérablement réduit la profondeur du test de Hadamard grâce à une combinaison de l'approximation de Trotter et d'unitaires non contrôlées.
Étape 3 : Exécuter à l'aide des primitives Qiskit
Instancier le backend et définir les paramètres d'exécution
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
if (
"if_else" not in backend.target.operation_names
): # Needed as "op_name" could be "if_else"
backend.target.add_instruction(IfElseOp, name="if_else")
print(backend.name)
Transpilation vers un QPU
Tout d'abord, sélectionnons des sous-ensembles de la carte de couplage contenant des qubits aux « bonnes » performances (où « bonnes » est assez arbitraire ici, nous souhaitons surtout éviter les qubits aux performances vraiment médiocres) et créons une nouvelle cible pour la transpilation.
target = backend.target
cmap = target.build_coupling_map(filter_idle_qubits=True)
cmap_list = list(cmap.get_edges())
cust_cmap_list = copy.deepcopy(cmap_list)
for q in range(target.num_qubits):
meas_err = target["measure"][(q,)].error
t2 = target.qubit_properties[q].t2 * 1e6
if meas_err > 0.02 or t2 < 100:
for q_pair in cmap_list:
if q in q_pair:
try:
cust_cmap_list.remove(q_pair)
except:
continue
for q in cmap_list:
op_name = list(target.operation_names_for_qargs(q))[0]
twoq_gate_err = target[f"{op_name}"][q].error
if twoq_gate_err > 0.005:
for q_pair in cmap_list:
if q == q_pair:
try:
cust_cmap_list.remove(q)
except:
continue
cust_cmap = CouplingMap(cust_cmap_list)
cust_target = Target.from_configuration(
basis_gates=backend.configuration().basis_gates,
coupling_map=cust_cmap,
)
Ensuite, transpile le circuit virtuel vers la meilleure disposition physique dans cette nouvelle cible.
basis_gates = list(target.operation_names)
pm = generate_preset_pass_manager(
optimization_level=3,
target=cust_target,
basis_gates=basis_gates,
)
qc_trans = pm.run(qc)
print("depth", qc_trans.depth(lambda x: x[0].num_qubits == 2))
print("num 2q ops", qc_trans.count_ops())
print(
"physical qubits",
sorted(
[
idx
for idx, qb in qc_trans.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
),
)
depth 52
num 2q ops OrderedDict([('rz', 2058), ('sx', 1703), ('cz', 728), ('x', 84), ('barrier', 8)])
physical qubits [91, 92, 93, 94, 95, 98, 99, 108, 109, 110, 111, 113, 114, 115, 119, 127, 132, 133, 134, 135, 137, 139, 147, 148, 149, 150, 151, 152, 153, 154, 155]
Créer les PUB pour l'exécution avec l'Estimator
# Define observables to measure for S
observable_S_real = "I" * (n_qubits) + "X"
observable_S_imag = "I" * (n_qubits) + "Y"
observable_op_real = SparsePauliOp(
observable_S_real
) # define a sparse pauli operator for the observable
observable_op_imag = SparsePauliOp(observable_S_imag)
layout = qc_trans.layout # get layout of transpiled circuit
observable_op_real = observable_op_real.apply_layout(
layout
) # apply physical layout to the observable
observable_op_imag = observable_op_imag.apply_layout(layout)
observable_S_real = (
observable_op_real.paulis.to_labels()
) # get the label of the physical observable
observable_S_imag = observable_op_imag.paulis.to_labels()
observables_S = [[observable_S_real], [observable_S_imag]]
# Define observables to measure for H
# Hamiltonian terms to measure
observable_list = []
for pauli, coeff in zip(H_op.paulis, H_op.coeffs):
# print(pauli)
observable_H_real = pauli[::-1].to_label() + "X"
observable_H_imag = pauli[::-1].to_label() + "Y"
observable_list.append([observable_H_real])
observable_list.append([observable_H_imag])
layout = qc_trans.layout
observable_trans_list = []
for observable in observable_list:
observable_op = SparsePauliOp(observable)
observable_op = observable_op.apply_layout(layout)
observable_trans_list.append([observable_op.paulis.to_labels()])
observables_H = observable_trans_list
# Define a sweep over parameter values
params = np.vstack(parameters).T
# Estimate the expectation value for all combinations of
# observables and parameter values, where the pub result will have
# shape (# observables, # parameter values).
pub = (qc_trans, observables_S + observables_H, params)
Exécuter les circuits
Les circuits pour sont calculables classiquement
qc_cliff = qc.assign_parameters({t: 0})
# Get expectation values from experiment
S_expval_real = StabilizerState(qc_cliff).expectation_value(
Pauli("I" * (n_qubits) + "X")
)
S_expval_imag = StabilizerState(qc_cliff).expectation_value(
Pauli("I" * (n_qubits) + "Y")
)
# Get expectation values
S_expval = S_expval_real + 1j * S_expval_imag
H_expval = 0
for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):
# Get expectation values from experiment
expval_real = StabilizerState(qc_cliff).expectation_value(
Pauli(pauli[::-1].to_label() + "X")
)
expval_imag = StabilizerState(qc_cliff).expectation_value(
Pauli(pauli[::-1].to_label() + "Y")
)
expval = expval_real + 1j * expval_imag
# Fill-in matrix elements
H_expval += coeff * expval
print(H_expval)
(25+0j)
Exécuter les circuits pour et avec l'Estimator
# Experiment options
num_randomizations = 300
num_randomizations_learning = 30
shots_per_randomization = 100
noise_factors = [1, 1.2, 1.4]
learning_pair_depths = [0, 4, 24, 48]
experimental_opts = {}
experimental_opts["resilience"] = {
"measure_mitigation": True,
"measure_noise_learning": {
"num_randomizations": num_randomizations_learning,
"shots_per_randomization": shots_per_randomization,
},
"zne_mitigation": True,
"zne": {"noise_factors": noise_factors},
"layer_noise_learning": {
"max_layers_to_learn": 10,
"layer_pair_depths": learning_pair_depths,
"shots_per_randomization": shots_per_randomization,
"num_randomizations": num_randomizations_learning,
},
"zne": {
"amplifier": "pea",
"extrapolated_noise_factors": [0] + noise_factors,
},
}
experimental_opts["twirling"] = {
"num_randomizations": num_randomizations,
"shots_per_randomization": shots_per_randomization,
"strategy": "all",
}
estimator = Estimator(mode=backend, options=experimental_opts)
job = estimator.run([pub])
Étape 4 : Post-traitement et renvoi du résultat dans le format classique souhaité
results = job.result()[0]
Calculer les matrices de Hamiltonien effectif et de recouvrement
Calcule d'abord la phase accumulée par l'état durant l'évolution temporelle non contrôlée
prefactors = [
np.exp(-1j * sum([c for p, c in H_op.to_list() if "Z" in p]) * i * dt)
for i in range(1, krylov_dim)
]
Une fois que nous avons les résultats des exécutions de circuits, nous pouvons post-traiter les données pour calculer les éléments de matrice de
# Assemble S, the overlap matrix of dimension D:
S_first_row = np.zeros(krylov_dim, dtype=complex)
S_first_row[0] = 1 + 0j
# Add in ancilla-only measurements:
for i in range(krylov_dim - 1):
# Get expectation values from experiment
expval_real = results.data.evs[0][0][
i
] # automatic extrapolated evs if ZNE is used
expval_imag = results.data.evs[1][0][
i
] # automatic extrapolated evs if ZNE is used
# Get expectation values
expval = expval_real + 1j * expval_imag
S_first_row[i + 1] += prefactors[i] * expval
S_first_row_list = S_first_row.tolist() # for saving purposes
S_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)
# Distribute entries from first row across matrix:
for i, j in it.product(range(krylov_dim), repeat=2):
if i >= j:
S_circ[j, i] = S_first_row[i - j]
else:
S_circ[j, i] = np.conj(S_first_row[j - i])
Matrix(S_circ)
Et les éléments de matrice de
# Assemble S, the overlap matrix of dimension D:
H_first_row = np.zeros(krylov_dim, dtype=complex)
H_first_row[0] = H_expval
for obs_idx, (pauli, coeff) in enumerate(zip(H_op.paulis, H_op.coeffs)):
# Add in ancilla-only measurements:
for i in range(krylov_dim - 1):
# Get expectation values from experiment
expval_real = results.data.evs[2 + 2 * obs_idx][0][
i
] # automatic extrapolated evs if ZNE is used
expval_imag = results.data.evs[2 + 2 * obs_idx + 1][0][
i
] # automatic extrapolated evs if ZNE is used
# Get expectation values
expval = expval_real + 1j * expval_imag
H_first_row[i + 1] += prefactors[i] * coeff * expval
H_first_row_list = H_first_row.tolist()
H_eff_circ = np.zeros((krylov_dim, krylov_dim), dtype=complex)
# Distribute entries from first row across matrix:
for i, j in it.product(range(krylov_dim), repeat=2):
if i >= j:
H_eff_circ[j, i] = H_first_row[i - j]
else:
H_eff_circ[j, i] = np.conj(H_first_row[j - i])
Matrix(H_eff_circ)
Enfin, nous pouvons résoudre le problème de valeurs propres généralisé pour :
et obtenir une estimation de l'énergie de l'état fondamental
gnd_en_circ_est_list = []
for d in range(1, krylov_dim + 1):
# Solve generalized eigenvalue problem for different size of the Krylov space
gnd_en_circ_est = solve_regularized_gen_eig(
H_eff_circ[:d, :d], S_circ[:d, :d], threshold=9e-1
)
gnd_en_circ_est_list.append(gnd_en_circ_est)
print("The estimated ground state energy is: ", gnd_en_circ_est)
The estimated ground state energy is: 25.0
The estimated ground state energy is: 22.572154819954875
The estimated ground state energy is: 21.691509219286587
The estimated ground state energy is: 21.23882298756386
The estimated ground state energy is: 20.965499325470294
Pour un secteur à particule unique, nous pouvons calculer efficacement l'état fondamental de ce secteur du Hamiltonien de manière classique
gs_en = single_particle_gs(H_op, n_qubits)
n_sys_qubits 30
n_exc 1 , subspace dimension 31
single particle ground state energy: 21.021912418526906
plt.plot(
range(1, krylov_dim + 1),
gnd_en_circ_est_list,
color="blue",
linestyle="-.",
label="KQD estimate",
)
plt.plot(
range(1, krylov_dim + 1),
[gs_en] * krylov_dim,
color="red",
linestyle="-",
label="exact",
)
plt.xticks(range(1, krylov_dim + 1), range(1, krylov_dim + 1))
plt.legend()
plt.xlabel("Krylov space dimension")
plt.ylabel("Energy")
plt.title(
"Estimating Ground state energy with Krylov Quantum Diagonalization"
)
plt.show()
Annexe : Sous-espace de Krylov à partir d'évolutions temporelles réelles
L'espace de Krylov unitaire est défini comme
pour un pas de temps que nous déterminerons ultérieurement. Supposons temporairement que est pair : définissons alors . Remarque que lorsque nous projetons le Hamiltonien dans l'espace de Krylov ci-dessus, celui-ci est indiscernable de l'espace de Krylov
c'est-à-dire celui où toutes les évolutions temporelles sont décalées en arrière de pas de temps. La raison pour laquelle il est indiscernable est que les éléments de matrice
sont invariants sous des décalages globaux du temps d'évolution, puisque les évolutions temporelles commutent avec le Hamiltonien. Pour impair, nous pouvons utiliser l'analyse pour .
Nous souhaitons montrer que quelque part dans cet espace de Krylov, il existe un état de basse énergie garanti. Nous le faisons par le biais du résultat suivant, dérivé du Théorème 3.1 dans [3] :
Affirmation 1 : il existe une fonction telle que pour les énergies dans la plage spectrale du Hamiltonien (c'est-à-dire entre l'énergie de l'état fondamental et l'énergie maximale)...
- pour toutes les valeurs de situées à une distance de , c'est-à-dire qu'elle est exponentiellement supprimée
- est une combinaison linéaire de pour
Nous fournissons une preuve ci-dessous, mais celle-ci peut être ignorée sans risque à moins que tu ne souhaites comprendre l'argument rigoureux complet. Pour l'instant, nous nous concentrons sur les implications de l'affirmation ci-dessus. Par la propriété 3 ci-dessus, nous pouvons voir que l'espace de Krylov décalé ci-dessus contient l'état . C'est notre état de basse énergie. Pour comprendre pourquoi, écrivons dans la base des états propres d'énergie :
où est le k-ième état propre d'énergie et est son amplitude dans l'état initial . Exprimé en ces termes, est donné par
en utilisant le fait que nous pouvons remplacer par lorsqu'il agit sur l'état propre . L'erreur en énergie de cet état est donc
Pour transformer ceci en une borne supérieure plus facile à comprendre, nous séparons d'abord la somme au numérateur en termes avec et termes avec :
Nous pouvons borner supérieurement le premier terme par ,
où la première étape découle du fait que pour chaque dans la somme, et la seconde étape découle du fait que la somme au numérateur est un sous-ensemble de la somme au dénominateur. Pour le second terme, nous bornons inférieurement le dénominateur par , puisque : en rassemblant le tout, cela donne
Pour simplifier ce qui reste, remarquez que pour tous ces , par la définition de , nous savons que . En bornant de plus supérieurement et en bornant supérieurement , on obtient
Ceci est valable pour tout , donc si nous fixons égal à notre erreur cible, alors la borne d'erreur ci-dessus converge vers cette valeur exponentiellement avec la dimension de l'espace de Krylov . Note également que si , alors le terme en disparaît entièrement dans la borne ci-dessus.
Pour compléter l'argument, nous notons d'abord que ce qui précède n'est que l'erreur en énergie de l'état particulier , plutôt que l'erreur en énergie de l'état de plus basse énergie dans l'espace de Krylov. Cependant, par le principe variationnel (de Rayleigh-Ritz), l'erreur en énergie de l'état de plus basse énergie dans l'espace de Krylov est bornée supérieurement par l'erreur en énergie de tout état dans l'espace de Krylov, donc ce qui précède est également une borne supérieure sur l'erreur en énergie de l'état de plus basse énergie, c'est-à-dire la sortie de l'algorithme de diagonalisation quantique de Krylov.
Une analyse similaire à celle ci-dessus peut être menée en tenant compte additionnellement du bruit et de la procédure de seuillage discutée dans ce notebook. Voir [2] et [4] pour cette analyse.
Annexe : preuve de l'Affirmation 1
Ce qui suit est principalement dérivé de [3], Théorème 3.1 : soit et soit l'espace des polynômes résiduels (polynômes dont la valeur en 0 est 1) de degré au plus . La solution de
est
et la valeur minimale correspondante est
Nous souhaitons convertir ceci en une fonction qui puisse être exprimée naturellement en termes d'exponentielles complexes, car ce sont les évolutions temporelles réelles qui engendrent l'espace quantique de Krylov. Pour ce faire, il est commode d'introduire la transformation suivante des énergies dans la plage spectrale du Hamiltonien vers des nombres dans l'intervalle : définissons
où est un pas de temps tel que . Remarque que et que croît à mesure que s'éloigne de .
En utilisant maintenant le polynôme avec les paramètres a, b, d fixés à , , et d = int(r/2), nous définissons la fonction :
où est l'énergie de l'état fondamental. Nous pouvons voir en insérant que est un polynôme trigonométrique de degré , c'est-à-dire une combinaison linéaire de pour . De plus, d'après la définition de ci-dessus, nous avons que et pour toute énergie dans la plage spectrale telle que , nous avons
Références
[1] N. Yoshioka, M. Amico, W. Kirby et al. "Diagonalization of large many-body Hamiltonians on a quantum processor". arXiv:2407.14431
[2] Ethan N. Epperly, Lin Lin, and Yuji Nakatsukasa. "A theory of quantum subspace diagonalization". SIAM Journal on Matrix Analysis and Applications 43, 1263–1290 (2022).
[3] Å. Björck. "Numerical methods in matrix computations". Texts in Applied Mathematics. Springer International Publishing. (2014).
[4] William Kirby. "Analysis of quantum Krylov algorithms with errors". Quantum 8, 1457 (2024).
Enquête sur le tutoriel
Réponds à cette courte enquête pour nous faire part de tes commentaires sur ce tutoriel. Tes retours nous aideront à améliorer nos contenus et l'expérience utilisateur.
Note : Cette enquête est proposée par IBM Quantum et porte sur le contenu du tutoriel (rédigé par IBM). doQumentation fournit le site web, les traductions et l'exécution du code — pour tout retour sur ces aspects, merci d'ouvrir une issue GitHub.