Aller au contenu principal

Formules multi-produits pour réduire l'erreur de Trotter

Utilisation estimée du QPU : quatre minutes sur un processeur Heron r2 (REMARQUE : il s'agit d'une estimation uniquement. Votre temps d'exécution peut varier.)

Contexte

Ce tutoriel démontre comment utiliser une Formule Multi-Produit (MPF) pour obtenir une erreur de Trotter plus faible sur notre observable par rapport à celle encourue par le circuit de Trotter le plus profond que nous exécuterons réellement. Les MPF réduisent l'erreur de Trotter de la dynamique hamiltonienne grâce à une combinaison pondérée de plusieurs exécutions de circuits. Considérez la tâche de trouver les valeurs d'espérance d'observable pour l'état quantique ρ(t)=eiHtρ(0)eiHt\rho(t)=e^{-i H t} \rho(0) e^{i H t} avec l'hamiltonien HH. On peut utiliser les Formules de Produit (PF) pour approximer l'évolution temporelle eiHte^{-i H t} en procédant comme suit :

  • Écrire l'hamiltonien HH comme H=a=1dFa,H=\sum_{a=1}^d F_a,FaF_a sont des opérateurs hermitiens tels que chaque unitaire correspondant puisse être implémenté efficacement sur un dispositif quantique.
  • Approximer les termes FaF_a qui ne commutent pas entre eux.

Ensuite, la PF de premier ordre (formule de Lie-Trotter) est :

S1(t):=a=1deiFat,S_1(t):=\prod_{a=1}^d e^{-i F_a t},

qui a un terme d'erreur quadratique S1(t)=eiHt+O(t2)S_1(t)=e^{-i H t}+\mathcal{O}\left(t^{2}\right). On peut également utiliser des PF d'ordre supérieur (formules de Lie-Trotter-Suzuki), qui convergent plus rapidement, et qui sont définies récursivement comme :

S2(t):=a=1deiFat/2a=1deiFat/2S_2(t):=\prod_{a=1}^d e^{-i F_a t/2}\prod_{a=1}^d e^{-i F_a t/2}

S2χ(t):=S2χ2(sχt)2S2χ2((14sχ)t)S2χ2(sχt)2,S_{2 \chi}(t):= S_{2 \chi -2}(s_{\chi}t)^2 S_{2 \chi -2}((1-4s_{\chi})t)S_{2 \chi -2}(s_{\chi}t)^2,

χ\chi est l'ordre de la PF symétrique et sp=(441/(2p1))1s_p = \left( 4 - 4^{1/(2p-1)} \right)^{-1}. Pour les évolutions temporelles longues, on peut diviser l'intervalle de temps tt en kk intervalles, appelés pas de Trotter, de durée t/kt/k et approximer l'évolution temporelle dans chaque intervalle avec une formule de produit d'ordre χ\chi SχS_{\chi}. Ainsi, la PF d'ordre χ\chi pour l'opérateur d'évolution temporelle sur kk pas de Trotter est :

Sχk(t)=[Sχ(tk)]k=eiHt+O(t(tk)χ) S_{\chi}^{k}(t) = \left[ S_{\chi} \left( \frac{t}{k} \right)\right]^k = e^{-i H t}+O\left(t \left( \frac{t}{k} \right)^{\chi} \right)

où le terme d'erreur diminue avec le nombre de pas de Trotter kk et l'ordre χ\chi de la PF.

Étant donné un entier k1k \geq 1 et une formule de produit Sχ(t)S_{\chi}(t), l'état approximatif évolué temporellement ρk(t)\rho_k(t) peut être obtenu à partir de ρ0\rho_0 en appliquant kk itérations de la formule de produit Sχ(tk)S_{\chi}\left(\frac{t}{k}\right).

ρk(t)=Sχ(tk)kρ0Sχ(tk)k\rho_k(t)=S_{\chi}\left(\frac{t}{k}\right)^k \rho_0 S_{\chi}\left(\frac{t}{k}\right)^{-k}

ρk(t)\rho_k(t) est une approximation de ρ(t)\rho(t) avec l'erreur d'approximation de Trotter ||ρk(t)ρ(t)\rho_k(t)-\rho(t) ||. Si nous considérons une combinaison linéaire d'approximations de Trotter de ρ(t)\rho(t) :

μ(t)=jlxjρjkj(tkj)+une certaine erreur de Trotter reˊsiduelle,\mu(t) = \sum_{j}^{l} x_j \rho^{k_j}_{j}\left(\frac{t}{k_j}\right) + \text{une certaine erreur de Trotter résiduelle} \, ,

xjx_j sont nos coefficients de pondération, ρjkj\rho^{k_j}_j est la matrice densité correspondant à l'état pur obtenu en faisant évoluer l'état initial avec la formule de produit, SχkjS^{k_j}_{\chi}, impliquant kjk_j pas de Trotter, et j1,...,lj \in {1, ..., l} indexe le nombre de PF qui composent la MPF. Tous les termes dans μ(t)\mu(t) utilisent la même formule de produit Sχ(t)S_{\chi}(t) comme base. L'objectif est d'améliorer ||ρk(t)ρ(t)\rho_k(t)-\rho(t) \| en trouvant μ(t)\mu(t) avec un μ(t)ρ(t)\|\mu(t)-\rho(t)\| encore plus faible.

  • μ(t)\mu(t) n'a pas besoin d'être un état physique car xix_i n'a pas besoin d'être positif. L'objectif ici est de minimiser l'erreur dans la valeur d'espérance des observables et non de trouver un remplacement physique pour ρ(t)\rho(t).
  • kjk_j détermine à la fois la profondeur du circuit et le niveau d'approximation de Trotter. Des valeurs plus petites de kjk_j conduisent à des circuits plus courts, qui encourent moins d'erreurs de circuit mais seront une approximation moins précise de l'état désiré.

La clé ici est que l'erreur de Trotter résiduelle donnée par μ(t)\mu(t) est plus petite que l'erreur de Trotter que l'on obtiendrait en utilisant simplement la plus grande valeur de kjk_j.

Vous pouvez voir l'utilité de ceci sous deux perspectives :

  1. Pour un budget fixe de pas de Trotter que vous pouvez exécuter, vous pouvez obtenir des résultats avec une erreur de Trotter qui est plus petite au total.
  2. Étant donné un nombre cible de pas de Trotter qui est trop grand pour être exécuté, vous pouvez utiliser la MPF pour trouver une collection de circuits de profondeur inférieure à exécuter qui résulte en une erreur de Trotter similaire.

Prérequis

Avant de commencer ce tutoriel, assurez-vous d'avoir les éléments suivants installés :

  • Qiskit SDK v1.0 ou version ultérieure, avec le support de visualisation
  • Qiskit Runtime v0.22 ou version ultérieure (pip install qiskit-ibm-runtime)
  • Addons Qiskit MPF (pip install qiskit_addon_mpf)
  • Addons Qiskit utils (pip install qiskit_addon_utils)
  • Bibliothèque Quimb (pip install quimb)
  • Bibliothèque Qiskit Quimb (pip install qiskit-quimb)
  • Numpy v0.21 pour la compatibilité entre les packages (pip install numpy==0.21)

Partie I. Exemple à petite échelle

Explorer la stabilité de la MPF

Il n'y a pas de restriction évidente sur le choix du nombre de pas de Trotter kjk_j qui composent l'état MPF μ(t)\mu(t). Cependant, ceux-ci doivent être choisis avec soin pour éviter les instabilités dans les valeurs d'espérance résultantes calculées à partir de μ(t)\mu(t). Une bonne règle générale est de définir le plus petit pas de Trotter kmink_{\text{min}} de sorte que t/kmin<1t/k_{\text{min}} \lt 1. Si vous souhaitez en savoir plus à ce sujet et sur la façon de choisir vos autres valeurs de kjk_j, consultez le guide Comment choisir les pas de Trotter pour une MPF.

Dans l'exemple ci-dessous, nous explorons la stabilité de la solution MPF en calculant la valeur d'espérance de la magnétisation pour une plage de temps en utilisant différents états évolués temporellement. Plus précisément, nous comparons les valeurs d'espérance calculées à partir de chacune des évolutions temporelles approximatives implémentées avec les pas de Trotter correspondants et les différents modèles MPF (coefficients statiques et dynamiques) avec les valeurs exactes de l'observable évolué temporellement. Tout d'abord, définissons les paramètres pour les formules de Trotter et les temps d'évolution

# Added by doQumentation — installs packages not in the Binder environment
%pip install -q qiskit-addon-mpf
import numpy as np

mpf_trotter_steps = [1, 2, 4]
order = 2
symmetric = False

trotter_times = np.arange(0.5, 1.55, 0.1)
exact_evolution_times = np.arange(trotter_times[0], 1.55, 0.05)

Pour cet exemple, nous utiliserons l'état de Néel comme état initial Neel=0101...01\vert \text{Neel} \rangle = \vert 0101...01 \rangle et le modèle de Heisenberg sur une ligne de 10 sites pour l'hamiltonien régissant l'évolution temporelle

H^Heis=Ji=1L1(XiX(i+1)+YiY(i+1)+ZiZ(i+1)),\hat{\mathcal{H}}_{Heis} = J \sum_{i=1}^{L-1} \left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ Z_i Z_{(i+1)} \right) \, ,

JJ est la force de couplage pour les arêtes de plus proches voisins.

from qiskit.transpiler import CouplingMap
from rustworkx.visualization import graphviz_draw
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
import numpy as np

L = 10

# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_line(L, bidirectional=False)
graphviz_draw(coupling_map.graph, method="circo")

# Get a qubit operator describing the Heisenberg field model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(1.0, 1.0, 1.0),
ext_magnetic_field=(0.0, 0.0, 0.0),
)

print(hamiltonian)
SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII'],
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])

L'observable que nous mesurerons est la magnétisation sur une paire de qubits au milieu de la chaîne.

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp.from_sparse_list(
[("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)
print(observable)
SparsePauliOp(['IIIIZZIIII'],
coeffs=[1.+0.j])

Nous définissons une passe de transpilation pour regrouper les rotations XX et YY dans le circuit en une seule porte XX+YY. Cela nous permettra de tirer parti des propriétés de conservation du spin de TeNPy lors du calcul MPO, accélérant considérablement le calcul.

from qiskit.circuit.library import XXPlusYYGate
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes.optimization.collect_and_collapse import (
CollectAndCollapse,
collect_using_filter_function,
collapse_to_operation,
)
from functools import partial

def filter_function(node):
return node.op.name in {"rxx", "ryy"}

collect_function = partial(
collect_using_filter_function,
filter_function=filter_function,
split_blocks=True,
min_block_size=1,
)

def collapse_to_xx_plus_yy(block):
param = 0.0
for node in block.data:
param += node.operation.params[0]
return XXPlusYYGate(param)

collapse_function = partial(
collapse_to_operation,
collapse_function=collapse_to_xx_plus_yy,
)

pm = PassManager()
pm.append(CollectAndCollapse(collect_function, collapse_function))

Ensuite, nous créons les circuits implémentant les évolutions temporelles approximatives de Trotter.

from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit import QuantumCircuit

# Initial Neel state preparation
initial_state_circ = QuantumCircuit(L)
initial_state_circ.x([i for i in range(L) if i % 2 != 0])

all_circs = []
for total_time in trotter_times:
mpf_trotter_circs = [
generate_time_evolution_circuit(
hamiltonian,
time=total_time,
synthesis=SuzukiTrotter(reps=num_steps, order=order),
)
for num_steps in mpf_trotter_steps
]

mpf_trotter_circs = pm.run(
mpf_trotter_circs
) # Collect XX and YY into XX + YY

mpf_circuits = [
initial_state_circ.compose(circuit) for circuit in mpf_trotter_circs
]
all_circs.append(mpf_circuits)
mpf_circuits[-1].draw("mpl", fold=-1)

Output of the previous code cell

Ensuite, nous calculons les valeurs d'espérance évoluées temporellement à partir des circuits de Trotter.

from copy import deepcopy
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

aer_sim = AerSimulator()
estimator = Estimator(mode=aer_sim)

mpf_expvals_all_times, mpf_stds_all_times = [], []
for t, mpf_circuits in zip(trotter_times, all_circs):
mpf_expvals = []
circuits = [deepcopy(circuit) for circuit in mpf_circuits]
pm_sim = generate_preset_pass_manager(
backend=aer_sim, optimization_level=3
)
isa_circuits = pm_sim.run(circuits)
result = estimator.run(
[(circuit, observable) for circuit in isa_circuits], precision=0.005
).result()
mpf_expvals = [res.data.evs for res in result]
mpf_stds = [res.data.stds for res in result]
mpf_expvals_all_times.append(mpf_expvals)
mpf_stds_all_times.append(mpf_stds)

Nous calculons également les valeurs d'espérance exactes pour comparaison.

from scipy.linalg import expm
from qiskit.quantum_info import Statevector

exact_expvals = []
for t in exact_evolution_times:
# Exact expectation values
exp_H = expm(-1j * t * hamiltonian.to_matrix())
initial_state = Statevector(initial_state_circ).data
time_evolved_state = exp_H @ initial_state

exact_obs = (
time_evolved_state.conj()
@ observable.to_matrix()
@ time_evolved_state
).real
exact_expvals.append(exact_obs)

Coefficients MPF statiques

Les MPF statiques sont celles où les valeurs xjx_j ne dépendent pas du temps d'évolution, tt. Considérons la PF d'ordre χ=1\chi = 1 avec kjk_j pas de Trotter, cela peut s'écrire comme :

S1kj(tkj)=eiHt+n=1Antn+1kjnS_1^{k_j}\left( \frac{t}{k_j} \right)=e^{-i H t}+ \sum_{n=1}^{\infty} A_n \frac{t^{n+1}}{k_j^n}

AnA_n sont des matrices qui dépendent des commutateurs des termes FaF_a dans la décomposition de l'hamiltonien. Il est important de noter que AnA_n eux-mêmes sont indépendants du temps et du nombre de pas de Trotter kjk_j. Par conséquent, il est possible d'annuler les termes d'erreur d'ordre inférieur contribuant à μ(t)\mu(t) avec un choix soigneux des poids xjx_j de la combinaison linéaire. Pour annuler l'erreur de Trotter pour les premiers l1l-1 termes (ceux-ci donneront les plus grandes contributions car ils correspondent au plus petit nombre de pas de Trotter) dans l'expression pour μ(t)\mu(t), les coefficients xjx_j doivent satisfaire les équations suivantes :

j=1lxj=1\sum_{j=1}^l x_j = 1 j=1l1xjkjn=0\sum_{j=1}^{l-1} \frac{x_j}{k_j^{n}} = 0

avec n=0,...l2n=0, ... l-2. La première équation garantit qu'il n'y a pas de biais dans l'état construit μ(t)\mu(t), tandis que la deuxième équation assure l'annulation des erreurs de Trotter. Pour les PF d'ordre supérieur, la deuxième équation devient j=1l1xjkjη=0\sum_{j=1}^{l-1} \frac{x_j}{k_j^{\eta}} = 0η=χ+2n\eta = \chi + 2n pour les PF symétriques et η=χ+n\eta = \chi + n sinon, avec n=0,...,l2n=0, ..., l-2. L'erreur résultante (Réf. [1],[2]) est alors

ϵ=O(tl+1k1l). \epsilon = \mathcal{O} \left( \frac{t^{l+1}}{k_1^l} \right).

Déterminer les coefficients MPF statiques pour un ensemble donné de valeurs kjk_j revient à résoudre le système linéaire d'équations défini par les deux équations ci-dessus pour les variables xjx_j : Ax=bAx=b. Où xx sont nos coefficients d'intérêt, AA est une matrice qui dépend de kjk_j et du type de PF que nous utilisons (SS), et bb est un vecteur de contraintes. Plus précisément :

A0,j=1A_{0,j} = 1 Ai>0,j=kj(χ+s(i1))A_{i>0,j} = k_{j}^{-(\chi + s(i-1))} b0=1b_0 = 1 bi>0=0b_{i>0} = 0

χ\chi est l'order, ss est 22 si symmetric est True et 11 sinon, kjk_{j} sont les trotter_steps, et xx sont les variables à résoudre. Les indices ii et jj commencent à 00. Nous pouvons également visualiser cela sous forme matricielle :

A=[A0,0A0,1A0,2...A1,0A1,1A1,2...A2,0A2,1A2,2...............]=[111...k0(χ+s(11))k1(χ+s(11))k2(χ+s(11))...k0(χ+s(21))k1(χ+s(21))k2(χ+s(21))...............]A = \begin{bmatrix} A_{0,0} & A_{0,1} & A_{0,2} & ... \\ A_{1,0} & A_{1,1} & A_{1,2} & ... \\ A_{2,0} & A_{2,1} & A_{2,2} & ... \\ ... & ... & ... & ... \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & ... \\ k_{0}^{-(\chi + s(1-1))} & k_{1}^{-(\chi + s(1-1))} & k_{2}^{-(\chi + s(1-1))} & ... \\ k_{0}^{-(\chi + s(2-1))} & k_{1}^{-(\chi + s(2-1))} & k_{2}^{-(\chi + s(2-1))} & ... \\ ... & ... & ... & ... \end{bmatrix}

et

b=[b0b1b2...]=[100...]b = \begin{bmatrix} b_{0} \\ b_{1} \\ b_{2} \\ ... \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \\ 0 \\ ... \end{bmatrix}

Pour plus de détails, consultez la documentation du Système Linéaire d'Équations (LSE).

Nous pouvons trouver une solution pour xx analytiquement comme x=A1bx = A^{-1}b ; voir par exemple les Réf. [1] ou [2]. Cependant, cette solution exacte peut être « mal conditionnée », résultant en de très grandes normes L1 de nos coefficients, xx, ce qui peut conduire à de mauvaises performances de la MPF. À la place, on peut également obtenir une solution approximative qui minimise la norme L1 de xx afin de tenter d'optimiser le comportement de la MPF.

Configurer le LSE

Maintenant que nous avons choisi nos valeurs kjk_j, nous devons d'abord construire le LSE, Ax=bAx=b comme expliqué ci-dessus. La matrice AA dépend non seulement de kjk_j mais aussi de notre choix de PF, en particulier son ordre. De plus, vous pouvez prendre en compte si la PF est symétrique ou non (voir [1]) en définissant symmetric=True/False. Cependant, cela n'est pas obligatoire, comme le montre la Réf. [2].

from qiskit_addon_mpf.static import setup_static_lse

lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)

Travaillons sur les valeurs choisies ci-dessus pour construire la matrice AA et le vecteur bb. Avec j=0,1,2j=0,1, 2 pas de Trotter kj=[1,2,4]k_j = [1, 2, 4], ordre χ=2\chi = 2 et choix de pas de Trotter non symétriques (s=1s=1), nous avons que les éléments de matrice de AA sous la première ligne sont déterminés par l'expression Ai>0,j=kj(2+1(i1))A_{i>0,j} = k_{j}^{-(2 + 1(i-1))}, spécifiquement :

A0,0=A0,1=A0,2=1A_{0,0} = A_{0,1} = A_{0,2} = 1 A1,j=kj1A1,0=112,  ,A1,1=122,  ,A1,2=142 A_{1,j} = k_{j}^{-1} \rightarrow A_{1,0} = \frac{1}{1^2}, \;, A_{1,1} = \frac{1}{2^2}, \;, A_{1,2} = \frac{1}{4^2} A2,j=kj2A2,0=113,  ,A2,1=123,  ,A2,2=143 A_{2,j} = k_{j}^{-2} \rightarrow A_{2,0} = \frac{1}{1^3}, \;, A_{2,1} = \frac{1}{2^3}, \;, A_{2,2} = \frac{1}{4^3}

ou sous forme matricielle :

A=[11111221421123143]A = \begin{bmatrix} 1 & 1 & 1\\ 1 & \frac{1}{2^2} & \frac{1}{4^2} \\ 1 & \frac{1}{2^3} & \frac{1}{4^3} \\ \end{bmatrix}

Cela est possible de voir en inspectant l'objet lse :

lse.A
array([[1.      , 1.      , 1.      ],
[1. , 0.25 , 0.0625 ],
[1. , 0.125 , 0.015625]])

Tandis que le vecteur de contraintes bb a les éléments suivants : b0=1b_{0} = 1 b1=b2=0b_1 = b_2 = 0

Ainsi,

b=[100]b = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix}

Et de manière similaire dans lse :

lse.b
array([1., 0., 0.])

L'objet lse a des méthodes pour trouver les coefficients statiques xjx_j satisfaisant le système d'équations.

mpf_coeffs = lse.solve()
print(
f"The static coefficients associated with the ansatze are: {mpf_coeffs}"
)
The static coefficients associated with the ansatze are: [ 0.04761905 -0.57142857  1.52380952]
Optimiser pour xx en utilisant un modèle exact

Alternativement au calcul de x=A1bx=A^{-1}b, vous pouvez également utiliser setup_exact_model pour construire une instance de cvxpy.Problem qui utilise le LSE comme contraintes et dont la solution optimale donnera xx.

Dans la section suivante, il sera clair pourquoi cette interface existe.

from qiskit_addon_mpf.costs import setup_exact_problem

model_exact, coeffs_exact = setup_exact_problem(lse)
model_exact.solve()
print(coeffs_exact.value)
[ 0.04761905 -0.57142857  1.52380952]

Comme indicateur pour savoir si une MPF construite avec ces coefficients donnera de bons résultats, nous pouvons utiliser la norme L1 (voir également Réf. [1]).

print(
"L1 norm of the exact coefficients:",
np.linalg.norm(coeffs_exact.value, ord=1),
) # ord specifies the norm. ord=1 is for L1
L1 norm of the exact coefficients: 2.1428571428556378
Optimiser pour xx en utilisant un modèle approximatif

Il peut arriver que la norme L1 pour l'ensemble choisi de valeurs kjk_j soit jugée trop élevée. Si c'est le cas et que vous ne pouvez pas choisir un ensemble différent de valeurs kjk_j, vous pouvez utiliser une solution approximative au LSE au lieu d'une exacte.

Pour ce faire, utilisez simplement setup_approximate_model pour construire une instance différente de cvxpy.Problem, qui contraint la norme L1 à un seuil choisi tout en minimisant la différence de AxAx et bb.

from qiskit_addon_mpf.costs import setup_sum_of_squares_problem

model_approx, coeffs_approx = setup_sum_of_squares_problem(
lse, max_l1_norm=1.5
)
model_approx.solve()
print(coeffs_approx.value)
print(
"L1 norm of the approximate coefficients:",
np.linalg.norm(coeffs_approx.value, ord=1),
)
[-1.10294118e-03 -2.48897059e-01  1.25000000e+00]
L1 norm of the approximate coefficients: 1.5

Notez que vous avez une liberté complète sur la façon de résoudre ce problème d'optimisation, ce qui signifie que vous pouvez changer le solveur d'optimisation, ses seuils de convergence, etc. Consultez le guide respectif sur Comment utiliser le modèle approximatif.

Coefficients MPF dynamiques

Dans la section précédente, nous avons introduit une MPF statique qui améliore l'approximation de Trotter standard. Cependant, cette version statique ne minimise pas nécessairement l'erreur d'approximation. Concrètement, la MPF statique, notée μS(t)\mu^S(t), n'est pas la projection optimale de ρ(t)\rho(t) sur le sous-espace engendré par les états de formule de produit {ρki(t)}i=1r\{\rho_{k_i}(t)\}_{i=1}^r.

Pour remédier à cela, nous considérons une MPF dynamique (introduite dans la Réf. [2] et démontrée expérimentalement dans la Réf. [3]) qui minimise l'erreur d'approximation dans la norme de Frobenius. Formellement, nous nous concentrons sur la minimisation de

ρ(t)μD(t)F2  =  Tr[(ρ(t)μD(t))2],\|\rho(t) - \mu^D(t)\|_F^2 \;=\; \mathrm{Tr}\bigl[ \left( \rho(t) - \mu^D(t)\right)^2 \bigr],

par rapport à certains coefficients xi(t)x_i(t) à chaque temps tt. Le projecteur optimal dans la norme de Frobenius est alors μD(t)  =  i=1rxi(t)ρki(t)\mu^D(t) \;=\; \sum_{i=1}^r x_i(t)\,\rho_{k_i}(t), et nous appelons μD(t)\mu^D(t) la MPF dynamique. En insérant les définitions ci-dessus :

ρ(t)μD(t)F2  =  =Tr[(ρ(t)μD(t))2]  =  =Tr[(ρ(t)i=1rxi(t)ρki(t))(ρ(t)j=1rxj(t)ρkj(t))]  =  =1  +  i,j=1rMi,j(t)xi(t)xj(t)    2i=1rLiexact(t)xi(t),\|\rho(t) - \mu^D(t)\|_F^2 \;=\; \\ = \mathrm{Tr}\bigl[ \left( \rho(t) - \mu^D(t)\right)^2 \bigr] \;=\; \\ = \mathrm{Tr}\bigl[ \left( \rho(t) - \sum_{i=1}^r x_i(t)\,\rho_{k_i}(t) \right) \left( \rho(t) - \sum_{j=1}^r x_j(t)\,\rho_{k_j}(t) \right) \bigr] \;=\; \\ = 1 \;+\; \sum_{i,j=1}^r M_{i,j}(t)\,x_i(t)\,x_j(t) \;-\; 2 \sum_{i=1}^r L_i^{\mathrm{exact}}(t)\,x_i(t),

Mi,j(t)M_{i,j}(t) est la matrice de Gram, définie par

Mi,j(t)  =  Tr[ρki(t)ρkj(t)]  =  ψin ⁣S(t/ki)kiS(t/kj)kj ⁣ψin2.M_{i,j}(t) \;=\; \mathrm{Tr}\bigl[\rho_{k_i}(t)\,\rho_{k_j}(t)\bigr] \;=\; \bigl|\langle \psi_{\mathrm{in}} \!\mid S\bigl(t/k_i\bigr)^{-k_i}\,S\bigl(t/k_j\bigr)^{k_j} \!\mid \psi_{\mathrm{in}} \rangle \bigr|^2.

et

Liexact(t)=Tr[ρ(t)ρki(t)]L_i^{\mathrm{exact}}(t) = \mathrm{Tr}[\rho(t)\,\rho_{k_i}(t)]

représente le recouvrement entre l'état exact ρ(t)\rho(t) et chaque approximation de formule de produit ρki(t)\rho_{k_i}(t). Dans des scénarios pratiques, ces recouvrements peuvent seulement être mesurés approximativement en raison du bruit ou de l'accès partiel à ρ(t)\rho(t).

Ici, ψin\lvert\psi_{\mathrm{in}}\rangle est l'état initial, et S()S(\cdot) est l'opération appliquée dans la formule de produit. En choisissant les coefficients xi(t)x_i(t) qui minimisent cette expression (et en gérant les données de recouvrement approximatives lorsque ρ(t)\rho(t) n'est pas entièrement connu), nous obtenons la « meilleure » (au sens de la norme de Frobenius) approximation dynamique de ρ(t)\rho(t) dans le sous-espace MPF. Les quantités Li(t)L_i(t) et Mi,j(t)M_{i,j}(t) peuvent être calculées efficacement en utilisant des méthodes de réseau de tenseurs [3]. L'addon Qiskit MPF fournit plusieurs « backends » pour effectuer le calcul. L'exemple ci-dessous montre la manière la plus flexible de le faire, et la documentation du backend basé sur les couches TeNPy explique également en grand détail. Pour utiliser cette méthode, commencez par le circuit implémentant l'évolution temporelle souhaitée et créez des modèles qui représentent ces opérations à partir des couches du circuit correspondant. Enfin, un objet Evolver est créé qui peut être utilisé pour générer les quantités évoluées temporellement Mi,j(t)M_{i,j}(t) et Li(t)L_i(t). Nous commençons par créer l'objet Evolver correspondant à l'évolution temporelle approximative (ApproxEvolverFactory) implémentée par les circuits. En particulier, faites très attention à la variable order pour qu'elles correspondent. Notez que lors de la génération des circuits correspondant à l'évolution temporelle approximative, nous utilisons des valeurs de substitution pour le time = 1.0 et le nombre de pas de Trotter (reps=1). Les circuits d'approximation corrects sont ensuite produits par le solveur de problème dynamique dans setup_dynamic_lse.

from qiskit_addon_utils.slicing import slice_by_depth
from qiskit_addon_mpf.backends.tenpy_layers import LayerModel
from qiskit_addon_mpf.backends.tenpy_layers import LayerwiseEvolver
from functools import partial

# Create approximate time-evolution circuits
single_2nd_order_circ = generate_time_evolution_circuit(
hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)
)
single_2nd_order_circ = pm.run(single_2nd_order_circ) # collect XX and YY

# Find layers in the circuit
layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)

# Create tensor network models
models = [
LayerModel.from_quantum_circuit(layer, conserve="Sz") for layer in layers
]

# Create the time-evolution object
approx_factory = partial(
LayerwiseEvolver,
layers=models,
options={
"preserve_norm": False,
"trunc_params": {
"chi_max": 64,
"svd_min": 1e-8,
"trunc_cut": None,
},
"max_delta_t": 2,
},
)
avertissement

Les options de LayerwiseEvolver qui déterminent les détails de la simulation de réseau de tenseurs doivent être choisies avec soin pour éviter de configurer un problème d'optimisation mal défini.

Nous configurons ensuite l'évoluteur exact (par exemple, ExactEvolverFactory), qui renvoie un objet Evolver calculant l'évolution temporelle vraie ou « de référence ». De manière réaliste, nous approximerions l'évolution exacte en utilisant une formule de Suzuki-Trotter d'ordre supérieur ou une autre méthode fiable avec un petit pas de temps. Ci-dessous, nous approximons l'état évolué temporellement exact avec une formule de Suzuki-Trotter de quatrième ordre en utilisant un petit pas de temps dt=0.1, ce qui signifie que le nombre de pas de Trotter utilisés au temps tt est k=t/dtk=t/dt. Nous spécifions également certaines options de troncature spécifiques à TeNPy pour limiter la dimension de liaison maximale du réseau de tenseurs sous-jacent, ainsi que les valeurs singulières minimales des liaisons de réseau de tenseurs divisées. Ces paramètres peuvent affecter la précision de la valeur d'espérance calculée avec les coefficients MPF dynamiques, il est donc important d'explorer une plage de valeurs pour trouver l'équilibre optimal entre le temps de calcul et la précision. Notez que le calcul des coefficients MPF ne repose pas sur la valeur d'espérance de la PF obtenue à partir de l'exécution matérielle, et peut donc être ajusté en post-traitement.

single_4th_order_circ = generate_time_evolution_circuit(
hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)
)
single_4th_order_circ = pm.run(single_4th_order_circ)
exact_model_layers = [
LayerModel.from_quantum_circuit(layer, conserve="Sz")
for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)
]

exact_factory = partial(
LayerwiseEvolver,
layers=exact_model_layers,
dt=0.1,
options={
"preserve_norm": False,
"trunc_params": {
"chi_max": 64,
"svd_min": 1e-8,
"trunc_cut": None,
},
"max_delta_t": 2,
},
)

Ensuite, créez l'état initial de votre système dans un format compatible avec TeNPy (par exemple, un MPS_neel_state=0101...01\vert 0101...01 \rangle). Cela configure la fonction d'onde à plusieurs corps que vous ferez évoluer dans le temps ψin\lvert\psi_{\mathrm{in}}\rangle comme un tenseur.

from qiskit_addon_mpf.backends.tenpy_tebd import MPOState
from qiskit_addon_mpf.backends.tenpy_tebd import MPS_neel_state

def identity_factory():
return MPOState.initialize_from_lattice(models[0].lat, conserve=True)

mps_initial_state = MPS_neel_state(models[0].lat)

Pour chaque pas de temps tt, nous configurons le système linéaire d'équations dynamique avec la méthode setup_dynamic_lse. L'objet correspondant contient les informations sur le problème MPF dynamique : lse.A donne la matrice de Gram MM tandis que lse.b donne le recouvrement LL. Nous pouvons ensuite résoudre le LSE (lorsqu'il n'est pas mal défini) pour trouver les coefficients dynamiques en utilisant le setup_frobenius_problem. Il est important de noter la différence avec les coefficients statiques, qui ne dépendent que des détails de la formule de produit utilisée et sont indépendants des détails de l'évolution temporelle (hamiltonien et état initial).

from qiskit_addon_mpf.dynamic import setup_dynamic_lse
from qiskit_addon_mpf.costs import setup_frobenius_problem

mpf_dynamic_coeffs_list = []
for t in trotter_times:
print(f"Computing dynamic coefficients for time={t}")
lse = setup_dynamic_lse(
mpf_trotter_steps,
t,
identity_factory,
exact_factory,
approx_factory,
mps_initial_state,
)
problem, coeffs = setup_frobenius_problem(lse)
try:
problem.solve()
mpf_dynamic_coeffs_list.append(coeffs.value)
except Exception as error:
mpf_dynamic_coeffs_list.append(np.zeros(len(mpf_trotter_steps)))
print(error, "Calculation Failed for time", t)
print("")
Computing dynamic coefficients for time=0.5

Computing dynamic coefficients for time=0.6

Computing dynamic coefficients for time=0.7

Computing dynamic coefficients for time=0.7999999999999999

Computing dynamic coefficients for time=0.8999999999999999

Computing dynamic coefficients for time=0.9999999999999999

Computing dynamic coefficients for time=1.0999999999999999

Computing dynamic coefficients for time=1.1999999999999997

Computing dynamic coefficients for time=1.2999999999999998

Computing dynamic coefficients for time=1.4

Computing dynamic coefficients for time=1.4999999999999998

Enfin, tracez ces valeurs d'espérance tout au long du temps d'évolution.

import matplotlib.pyplot as plt

sym = {1: "^", 2: "s", 4: "p"}
# Get expectation values at all times for each Trotter step
for k, step in enumerate(mpf_trotter_steps):
trotter_curve, trotter_curve_error = [], []
for trotter_expvals, trotter_stds in zip(
mpf_expvals_all_times, mpf_stds_all_times
):
trotter_curve.append(trotter_expvals[k])
trotter_curve_error.append(trotter_stds[k])

plt.errorbar(
trotter_times,
trotter_curve,
yerr=trotter_curve_error,
alpha=0.5,
markersize=4,
marker=sym[step],
color="grey",
label=f"{mpf_trotter_steps[k]} Trotter steps",
) # , , )

# Get expectation values at all times for the static MPF with exact coeffs
exact_mpf_curve, exact_mpf_curve_error = [], []
for trotter_expvals, trotter_stds in zip(
mpf_expvals_all_times, mpf_stds_all_times
):
mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(coeffs_exact.value, trotter_stds)
]
)
)
exact_mpf_curve_error.append(mpf_std)
exact_mpf_curve.append(trotter_expvals @ coeffs_exact.value)

plt.errorbar(
trotter_times,
exact_mpf_curve,
yerr=exact_mpf_curve_error,
markersize=4,
marker="o",
label="Static MPF - Exact",
color="purple",
)

# Get expectation values at all times for the static MPF with approximate
approx_mpf_curve, approx_mpf_curve_error = [], []
for trotter_expvals, trotter_stds in zip(
mpf_expvals_all_times, mpf_stds_all_times
):
mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(coeffs_approx.value, trotter_stds)
]
)
)
approx_mpf_curve_error.append(mpf_std)
approx_mpf_curve.append(trotter_expvals @ coeffs_approx.value)

plt.errorbar(
trotter_times,
approx_mpf_curve,
yerr=approx_mpf_curve_error,
markersize=4,
marker="o",
color="orange",
label="Static MPF - Approximate",
)

# # Get expectation values at all times for the dynamic MPF
dynamic_mpf_curve, dynamic_mpf_curve_error = [], []
for trotter_expvals, trotter_stds, dynamic_coeffs in zip(
mpf_expvals_all_times, mpf_stds_all_times, mpf_dynamic_coeffs_list
):
mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(dynamic_coeffs, trotter_stds)
]
)
)
dynamic_mpf_curve_error.append(mpf_std)
dynamic_mpf_curve.append(trotter_expvals @ dynamic_coeffs)

plt.errorbar(
trotter_times,
dynamic_mpf_curve,
yerr=dynamic_mpf_curve_error,
markersize=4,
marker="o",
color="pink",
label="Dynamic MPF",
)

plt.plot(
exact_evolution_times,
exact_expvals,
lw=3,
color="red",
label="Exact time-evolution",
)

plt.title(
f"Expectation values for (ZZ,{(L//2-1, L//2)}) as a function of time"
)
plt.xlabel("Time")
plt.ylabel("Expectation Value")
plt.legend()
plt.grid()

Output of the previous code cell

Dans les cas comme l'exemple ci-dessus, où la PF k=1k=1 se comporte mal à tous les temps, la qualité des résultats MPF dynamiques est également fortement affectée. Dans de telles situations, il est utile d'étudier la possibilité d'utiliser des PF individuelles avec un nombre plus élevé de pas de Trotter pour améliorer la qualité globale des résultats. Dans ces simulations, nous voyons l'interaction de différents types d'erreurs : l'erreur due à l'échantillonnage fini, et l'erreur de Trotter due aux formules de produit. La MPF aide à réduire l'erreur de Trotter due aux formules de produit mais encourt une erreur d'échantillonnage plus élevée par rapport aux formules de produit. Cela peut être avantageux, car les formules de produit peuvent réduire l'erreur d'échantillonnage avec un échantillonnage accru, mais l'erreur systématique due à l'approximation de Trotter reste inchangée.

Un autre comportement intéressant que nous pouvons observer à partir du graphique est que la valeur d'espérance pour la PF pour k=1k=1 commence à se comporter de manière erratique (en plus de ne pas être une bonne approximation pour l'exacte) aux temps pour lesquels t/k>1t/k > 1 , comme expliqué dans le guide sur la façon de choisir le nombre de pas de Trotter.

Étape 1 : Convertir les entrées classiques en un problème quantique

Considérons maintenant un temps unique t=1.0t=1.0 et calculons la valeur d'espérance de la magnétisation avec les différentes méthodes en utilisant une seule QPU. Le choix particulier de tt a été fait afin de maximiser la différence entre les différentes méthodes et d'observer leur efficacité relative. Pour déterminer la fenêtre de temps pour laquelle la MPF dynamique est garantie de produire des observables avec une erreur plus faible que n'importe laquelle des formules de Trotter individuelles au sein du multi-produit, nous pouvons implémenter le "test MPF" - voir l'équation (17) et le texte environnant dans [3].

Configurer les circuits de Trotter

À ce stade, nous avons trouvé nos coefficients d'expansion, xx, et il ne reste plus qu'à générer les circuits quantiques trotterisés. Une fois de plus, le module qiskit_addon_utils.problem_generators vient à la rescousse avec une fonction utile pour faire cela :

from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit import QuantumCircuit

total_time = 1.0
mpf_circuits = []
for k in mpf_trotter_steps:
# Initial Neel state preparation
circuit = QuantumCircuit(L)
circuit.x([i for i in range(L) if i % 2 != 0])

trotter_circ = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(order=order, reps=k),
time=total_time,
)

circuit.compose(trotter_circ, inplace=True)

mpf_circuits.append(pm.run(circuit))
mpf_circuits[-1].draw("mpl", fold=-1, scale=0.4)

Output of the previous code cell

Étape 2 : Optimiser le problème pour l'exécution sur matériel quantique

Revenons au calcul de la valeur d'espérance pour un point temporel unique. Nous allons choisir un backend pour exécuter l'expérience sur le matériel.

from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(min_num_qubits=127)
print(backend)

qubits = list(range(backend.num_qubits))

Ensuite, nous retirons les qubits aberrants de la carte de couplage pour nous assurer que l'étape de disposition du transpilateur ne les inclut pas. Ci-dessous, nous utilisons les propriétés du backend rapportées stockées dans l'objet target et retirons les qubits qui ont soit une erreur de mesure ou une porte à deux qubits au-dessus d'un certain seuil (max_meas_err, max_twoq_err), soit un temps T2T_2 (qui détermine la perte de cohérence) en dessous d'un certain seuil (min_t2).

import copy
from qiskit.transpiler import Target, CouplingMap

target = backend.target
instruction_2q = "cz"

cmap = target.build_coupling_map(filter_idle_qubits=True)
cmap_list = list(cmap.get_edges())

max_meas_err = 0.012
min_t2 = 40
max_twoq_err = 0.005

# Remove qubits with bad measurement or t2
cust_cmap_list = copy.deepcopy(cmap_list)
for q in range(target.num_qubits):
meas_err = target["measure"][(q,)].error
if target.qubit_properties[q].t2 is not None:
t2 = target.qubit_properties[q].t2 * 1e6
else:
t2 = 0
if meas_err > max_meas_err or t2 < min_t2:
# print(q)
for q_pair in cmap_list:
if q in q_pair:
try:
cust_cmap_list.remove(q_pair)
except ValueError:
continue

# Remove qubits with bad 2q gate or t2
for q in cmap_list:
twoq_gate_err = target[instruction_2q][q].error
if twoq_gate_err > max_twoq_err:
# print(q)
for q_pair in cmap_list:
if q == q_pair:
try:
cust_cmap_list.remove(q_pair)
except ValueError:
continue

cust_cmap = CouplingMap(cust_cmap_list)

cust_target = Target.from_configuration(
basis_gates=backend.configuration().basis_gates
+ ["measure"], # or whatever new set of gates
coupling_map=cust_cmap,
)

sorted_components = sorted(
[list(comp.physical_qubits) for comp in cust_cmap.connected_components()],
reverse=True,
)
print("size of largest component", len(sorted_components[0]))
size of largest component 10

Nous voulons définir max_meas_err, min_t2 et max_twoq_err de manière à trouver un sous-ensemble de qubits suffisamment grand qui supporte l'exécution du circuit. Dans notre cas, il suffit de trouver une chaîne 1D de 10 qubits.

cust_cmap.draw()

Output of the previous code cell

Nous pouvons ensuite mapper le circuit et l'observable sur les qubits physiques du dispositif.

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

transpiler = generate_preset_pass_manager(
optimization_level=3, target=cust_target
)

transpiled_circuits = [transpiler.run(circ) for circ in mpf_circuits]

qubits_layouts = [
[
idx
for idx, qb in circuit.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
for circuit in transpiled_circuits
]

transpiled_circuits = []
for circuit, layout in zip(mpf_circuits, qubits_layouts):
transpiler = generate_preset_pass_manager(
optimization_level=3, backend=backend, initial_layout=layout
)
transpiled_circuit = transpiler.run(circuit)
transpiled_circuits.append(transpiled_circuit)

# transform the observable defined on virtual qubits to
# an observable defined on all physical qubits
isa_observables = [
observable.apply_layout(circ.layout) for circ in transpiled_circuits
]
print(transpiled_circuits[-1].depth(lambda x: x.operation.num_qubits == 2))
print(transpiled_circuits[-1].count_ops())
transpiled_circuits[-1].draw("mpl", idle_wires=False, fold=False)
51
OrderedDict([('sx', 310), ('rz', 232), ('cz', 132), ('x', 19)])

Output of the previous code cell

Étape 3 : Exécuter en utilisant les primitives Qiskit

Avec la primitive Estimator, nous pouvons obtenir l'estimation de la valeur d'espérance à partir de la QPU. Nous exécutons les circuits AQC optimisés avec des techniques supplémentaires d'atténuation et de suppression des erreurs.

from qiskit_ibm_runtime import EstimatorV2 as Estimator

estimator = Estimator(mode=backend)
estimator.options.default_shots = 30000

# Set simple error suppression/mitigation options
estimator.options.dynamical_decoupling.enable = True
estimator.options.twirling.enable_gates = True
estimator.options.twirling.enable_measure = True
estimator.options.twirling.num_randomizations = "auto"
estimator.options.twirling.strategy = "active-accum"
estimator.options.resilience.measure_mitigation = True
estimator.options.experimental.execution_path = "gen3-turbo"

estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")

estimator.options.environment.job_tags = ["mpf small"]

job = estimator.run(
[
(circ, observable)
for circ, observable in zip(transpiled_circuits, isa_observables)
]
)

Étape 4 : Post-traiter et renvoyer le résultat dans le format classique souhaité

La seule étape de post-traitement consiste à combiner la valeur d'espérance obtenue à partir des primitives Qiskit Runtime à différentes étapes de Trotter en utilisant les coefficients MPF respectifs. Pour un observable AA nous avons :

Ampf=Tr[Aμ(t)]=jxjTr[Aρkj]=jxjAj \langle A \rangle_{\text{mpf}} = \text{Tr} [A \mu(t)] = \sum_{j} x_j \text{Tr} [A \rho_{k_j}] = \sum_{j} x_j \langle A \rangle_j

Tout d'abord, nous extrayons les valeurs d'espérance individuelles obtenues pour chacun des circuits de Trotter :

result_exp = job.result()
evs_exp = [res.data.evs for res in result_exp]
evs_std = [res.data.stds for res in result_exp]

print(evs_exp)
[array(-0.06361607), array(-0.23820448), array(-0.50271805)]

Ensuite, nous les recombinons simplement avec nos coefficients MPF pour obtenir les valeurs d'espérance totales de la MPF. Ci-dessous, nous le faisons pour chacune des différentes manières par lesquelles nous avons calculé xx.

exact_mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(coeffs_exact.value, evs_std)
]
)
)
print(
"Exact static MPF expectation value: ",
evs_exp @ coeffs_exact.value,
"+-",
exact_mpf_std,
)
approx_mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(coeffs_approx.value, evs_std)
]
)
)
print(
"Approximate static MPF expectation value: ",
evs_exp @ coeffs_approx.value,
"+-",
approx_mpf_std,
)
dynamic_mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(mpf_dynamic_coeffs_list[7], evs_std)
]
)
)
print(
"Dynamic MPF expectation value: ",
evs_exp @ mpf_dynamic_coeffs_list[7],
"+-",
dynamic_mpf_std,
)
Exact static MPF expectation value:  -0.6329590442738475 +- 0.012798249760406036
Approximate static MPF expectation value: -0.5690390035339492 +- 0.010459559917168473
Dynamic MPF expectation value: -0.4655579758795695 +- 0.007639139186720507

Enfin, pour ce petit problème, nous pouvons calculer la valeur de référence exacte en utilisant scipy.linalg.expm comme suit :

from scipy.linalg import expm
from qiskit.quantum_info import Statevector

exp_H = expm(-1j * total_time * hamiltonian.to_matrix())

initial_state_circuit = QuantumCircuit(L)
initial_state_circuit.x([i for i in range(L) if i % 2 != 0])
initial_state = Statevector(initial_state_circuit).data

time_evolved_state = exp_H @ initial_state

exact_obs = (
time_evolved_state.conj() @ observable.to_matrix() @ time_evolved_state
)
print("Exact expectation value ", exact_obs.real)
Exact expectation value  -0.39909900734489434
sym = {1: "^", 2: "s", 4: "p"}
# Get expectation values at all times for each Trotter step
for k, step in enumerate(mpf_trotter_steps):
plt.errorbar(
k,
evs_exp[k],
yerr=evs_std[k],
alpha=0.5,
markersize=4,
marker=sym[step],
color="grey",
label=f"{mpf_trotter_steps[k]} Trotter steps",
) # , , )

plt.errorbar(
3,
evs_exp @ coeffs_exact.value,
yerr=exact_mpf_std,
markersize=4,
marker="o",
color="purple",
label="Static MPF",
)

plt.errorbar(
4,
evs_exp @ coeffs_approx.value,
yerr=approx_mpf_std,
markersize=4,
marker="o",
color="orange",
label="Approximate static MPF",
)

plt.errorbar(
5,
evs_exp @ mpf_dynamic_coeffs_list[7],
yerr=dynamic_mpf_std,
markersize=4,
marker="o",
color="pink",
label="Dynamic MPF",
)

plt.axhline(
y=exact_obs.real,
linestyle="--",
color="red",
label="Exact time-evolution",
)

plt.title(
f"Expectation values for (ZZ,{(L//2-1, L//2)}) at time {total_time} for the different methods "
)
plt.xlabel("Method")
plt.ylabel("Expectation Value")
plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.2), ncol=2)
plt.grid(alpha=0.1)
plt.tight_layout()
plt.show()

Output of the previous code cell

Dans l'exemple ci-dessus, la méthode MPF dynamique performe le mieux en termes de valeur d'espérance, s'améliorant par rapport à ce que nous aurions obtenu en utilisant le plus grand nombre d'étapes de Trotter seul. Même si les différentes techniques MPF n'obtiennent pas toujours une valeur d'espérance améliorée par rapport au plus grand nombre d'étapes de Trotter (comme le modèle exact et le modèle approximatif dans le graphique ci-dessus), l'écart type de ces valeurs capture bien l'augmentation de la variance encourue lors de l'utilisation de la technique MPF. Cela met en évidence l'incertitude autour de la valeur d'espérance obtenue, qui inclut toujours la valeur d'espérance que nous attendrions d'une évolution temporelle exacte du système. D'autre part, les valeurs d'espérance calculées avec le nombre inférieur d'étapes de Trotter ne parviennent pas à capturer la valeur d'espérance exacte dans leur incertitude, renvoyant ainsi avec certitude le mauvais résultat.

def relative_error(ev, exact_ev):
return abs(ev - exact_ev)

relative_error_k = [relative_error(ev, exact_obs.real) for ev in evs_exp]
relative_error_mpf = relative_error(evs_exp @ mpf_coeffs, exact_obs.real)
relative_error_approx_mpf = relative_error(
evs_exp @ coeffs_approx.value, exact_obs.real
)
relative_error_dynamic_mpf = relative_error(
evs_exp @ mpf_dynamic_coeffs_list[7], exact_obs.real
)

print("relative error for each trotter steps", relative_error_k)
print("relative error with MPF exact coeffs", relative_error_mpf)
print("relative error with MPF approx coeffs", relative_error_approx_mpf)
print("relative error with MPF dynamic coeffs", relative_error_dynamic_mpf)
relative error for each trotter steps [0.33548293650112293, 0.16089452939226306, 0.10361904247828346]
relative error with MPF exact coeffs 0.2338600369291003
relative error with MPF approx coeffs 0.16993999618905486
relative error with MPF dynamic coeffs 0.06645896853467514

Partie II : passage à l'échelle

Passons maintenant à un problème d'une échelle supérieure, au-delà de ce qui peut être simulé de manière exacte. Dans cette section, nous nous concentrerons sur la reproduction de certains résultats présentés dans la Réf. [3].

Étape 1 : Transformer les entrées classiques en un problème quantique

Hamiltonien

Pour l'exemple à grande échelle, nous utilisons le modèle XXZ sur une ligne de 50 sites :

H^XXZ=i=1L1Ji,(i+1)(XiX(i+1)+YiY(i+1)+2ZiZ(i+1)),\hat{\mathcal{H}}_{XXZ} = \sum_{i=1}^{L-1} J_{i,(i+1)}\left(X_i X_{(i+1)}+Y_i Y_{(i+1)}+ 2\cdot Z_i Z_{(i+1)} \right) \, ,

Ji,(i+1)J_{i,(i+1)} est un coefficient aléatoire correspondant à l'arête (i,i+1)(i, i+1). Il s'agit de l'hamiltonien considéré dans la démonstration présentée dans la Réf. [3].

L = 50
# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_line(L, bidirectional=False)
graphviz_draw(coupling_map.graph, method="circo")

Output of the previous code cell

import numpy as np
from qiskit.quantum_info import SparsePauliOp, Pauli

# Generate random coefficients for our XXZ Hamiltonian
np.random.seed(0)
even_edges = list(coupling_map.get_edges())[::2]
odd_edges = list(coupling_map.get_edges())[1::2]

Js = np.random.uniform(0.5, 1.5, size=L)
hamiltonian = SparsePauliOp(Pauli("I" * L))
for i, edge in enumerate(even_edges + odd_edges):
hamiltonian += SparsePauliOp.from_sparse_list(
[
("XX", (edge), 2 * Js[i]),
("YY", (edge), 2 * Js[i]),
("ZZ", (edge), 4 * Js[i]),
],
num_qubits=L,
)

print(hamiltonian)
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXX', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYY', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'XXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'YYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZI', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IXXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IYYIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1. +0.j, 2.09762701+0.j, 2.09762701+0.j, 4.19525402+0.j,
2.43037873+0.j, 2.43037873+0.j, 4.86075747+0.j, 2.20552675+0.j,
2.20552675+0.j, 4.4110535 +0.j, 2.08976637+0.j, 2.08976637+0.j,
4.17953273+0.j, 1.8473096 +0.j, 1.8473096 +0.j, 3.6946192 +0.j,
2.29178823+0.j, 2.29178823+0.j, 4.58357645+0.j, 1.87517442+0.j,
1.87517442+0.j, 3.75034885+0.j, 2.783546 +0.j, 2.783546 +0.j,
5.567092 +0.j, 2.92732552+0.j, 2.92732552+0.j, 5.85465104+0.j,
1.76688304+0.j, 1.76688304+0.j, 3.53376608+0.j, 2.58345008+0.j,
2.58345008+0.j, 5.16690015+0.j, 2.05778984+0.j, 2.05778984+0.j,
4.11557968+0.j, 2.13608912+0.j, 2.13608912+0.j, 4.27217824+0.j,
2.85119328+0.j, 2.85119328+0.j, 5.70238655+0.j, 1.14207212+0.j,
1.14207212+0.j, 2.28414423+0.j, 1.1742586 +0.j, 1.1742586 +0.j,
2.3485172 +0.j, 1.04043679+0.j, 1.04043679+0.j, 2.08087359+0.j,
2.66523969+0.j, 2.66523969+0.j, 5.33047938+0.j, 2.5563135 +0.j,
2.5563135 +0.j, 5.112627 +0.j, 2.7400243 +0.j, 2.7400243 +0.j,
5.48004859+0.j, 2.95723668+0.j, 2.95723668+0.j, 5.91447337+0.j,
2.59831713+0.j, 2.59831713+0.j, 5.19663426+0.j, 1.92295872+0.j,
1.92295872+0.j, 3.84591745+0.j, 2.56105835+0.j, 2.56105835+0.j,
5.12211671+0.j, 1.23654885+0.j, 1.23654885+0.j, 2.4730977 +0.j,
2.27984204+0.j, 2.27984204+0.j, 4.55968409+0.j, 1.28670657+0.j,
1.28670657+0.j, 2.57341315+0.j, 2.88933783+0.j, 2.88933783+0.j,
5.77867567+0.j, 2.04369664+0.j, 2.04369664+0.j, 4.08739329+0.j,
1.82932388+0.j, 1.82932388+0.j, 3.65864776+0.j, 1.52911122+0.j,
1.52911122+0.j, 3.05822245+0.j, 2.54846738+0.j, 2.54846738+0.j,
5.09693476+0.j, 1.91230066+0.j, 1.91230066+0.j, 3.82460133+0.j,
2.1368679 +0.j, 2.1368679 +0.j, 4.2737358 +0.j, 1.0375796 +0.j,
1.0375796 +0.j, 2.0751592 +0.j, 2.23527099+0.j, 2.23527099+0.j,
4.47054199+0.j, 2.22419145+0.j, 2.22419145+0.j, 4.44838289+0.j,
2.23386799+0.j, 2.23386799+0.j, 4.46773599+0.j, 2.88749616+0.j,
2.88749616+0.j, 5.77499231+0.j, 2.3636406 +0.j, 2.3636406 +0.j,
4.7272812 +0.j, 1.7190158 +0.j, 1.7190158 +0.j, 3.4380316 +0.j,
1.87406391+0.j, 1.87406391+0.j, 3.74812782+0.j, 2.39526239+0.j,
2.39526239+0.j, 4.79052478+0.j, 1.12045094+0.j, 1.12045094+0.j,
2.24090189+0.j, 2.33353343+0.j, 2.33353343+0.j, 4.66706686+0.j,
2.34127574+0.j, 2.34127574+0.j, 4.68255148+0.j, 1.42076512+0.j,
1.42076512+0.j, 2.84153024+0.j, 1.2578526 +0.j, 1.2578526 +0.j,
2.51570519+0.j, 1.6308567 +0.j, 1.6308567 +0.j, 3.2617134 +0.j])

Pour l'observable, nous choisissons Z24Z25Z_{24}Z_{25}, comme illustré dans le panneau inférieur de la Fig. 5 de la Réf. [3].

observable = SparsePauliOp.from_sparse_list(
[("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)
print(observable)
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIZZIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])

Choisir les étapes de Trotter

L'expérience présentée dans la Fig. 4 de la Réf. [3] utilise kj=[2,3,4]k_j = [2, 3, 4] étapes de Trotter symétriques d'ordre 22. Nous nous concentrons sur les résultats pour le temps t=3t=3, où la MPF et une PF avec un nombre plus élevé d'étapes de Trotter (6 dans ce cas) ont la même erreur de Trotter. Cependant, la valeur d'espérance de la MPF est calculée à partir de circuits correspondant au nombre inférieur d'étapes de Trotter et donc moins profonds. En pratique, même si la MPF et le circuit d'étapes de Trotter plus profond ont la même erreur de Trotter, nous nous attendons à ce que la valeur d'espérance expérimentale calculée à partir des circuits MPF soit plus proche de la valeur théorique, car elle implique l'exécution de circuits moins profonds, moins exposés au bruit matériel, comparés au circuit correspondant à la PF avec un nombre plus élevé d'étapes de Trotter.

total_time = 3
mpf_trotter_steps = [2, 3, 4]
order = 2
symmetric = True

Configurer la LSE

Nous examinons ici les coefficients MPF statiques pour ce problème.

lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)
mpf_coeffs = lse.solve()
print(
f"The static coefficients associated with the ansatze are: {mpf_coeffs}"
)
print("L1 norm:", np.linalg.norm(mpf_coeffs, ord=1))
The static coefficients associated with the ansatze are: [ 0.26666667 -2.31428571  3.04761905]
L1 norm: 5.628571428571431
model_approx, coeffs_approx = setup_sum_of_squares_problem(
lse, max_l1_norm=2.0
)
model_approx.solve()
print(coeffs_approx.value)
print(
"L1 norm of the approximate coefficients:",
np.linalg.norm(coeffs_approx.value, ord=1),
)
[-0.24255546 -0.25744454  1.5       ]
L1 norm of the approximate coefficients: 2.0

Coefficients dynamiques

# Create approximate time-evolution circuits
single_2nd_order_circ = generate_time_evolution_circuit(
hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)
)
single_2nd_order_circ = pm.run(single_2nd_order_circ) # collect XX and YY

# Find layers in the circuit
layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)

# Create tensor network models
models = [
LayerModel.from_quantum_circuit(layer, conserve="Sz") for layer in layers
]

# Create the time-evolution object
approx_factory = partial(
LayerwiseEvolver,
layers=models,
options={
"preserve_norm": False,
"trunc_params": {
"chi_max": 64,
"svd_min": 1e-8,
"trunc_cut": None,
},
"max_delta_t": 4,
},
)

# Create exact time-evolution circuits
single_4th_order_circ = generate_time_evolution_circuit(
hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)
)
single_4th_order_circ = pm.run(single_4th_order_circ)
exact_model_layers = [
LayerModel.from_quantum_circuit(layer, conserve="Sz")
for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)
]

# Create the time-evolution object
exact_factory = partial(
LayerwiseEvolver,
layers=exact_model_layers,
dt=0.1,
options={
"preserve_norm": False,
"trunc_params": {
"chi_max": 64,
"svd_min": 1e-8,
"trunc_cut": None,
},
"max_delta_t": 3,
},
)

def identity_factory():
return MPOState.initialize_from_lattice(models[0].lat, conserve=True)

mps_initial_state = MPS_neel_state(models[0].lat)

lse = setup_dynamic_lse(
mpf_trotter_steps,
total_time,
identity_factory,
exact_factory,
approx_factory,
mps_initial_state,
)
problem, coeffs = setup_frobenius_problem(lse)
try:
problem.solve()
mpf_dynamic_coeffs = coeffs.value
except Exception as error:
print(error, "Calculation Failed for time", total_time)
print("")

Construire chacun des circuits de Trotter dans notre décomposition MPF

from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit import QuantumCircuit

mpf_circuits = []
for k in mpf_trotter_steps:
# Initial state preparation |1010..>
circuit = QuantumCircuit(L)
circuit.x([i for i in range(L) if i % 2])

trotter_circ = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=k, order=order),
time=total_time,
)

circuit.compose(trotter_circ, qubits=range(L), inplace=True)

mpf_circuits.append(circuit)

Construire le circuit de Trotter avec une erreur de Trotter comparable à la MPF

k = 6

# Initial state preparation |1010..>
comp_circuit = QuantumCircuit(L)
comp_circuit.x([i for i in range(L) if i % 2])

trotter_circ = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=k, order=order),
time=total_time,
)

comp_circuit.compose(trotter_circ, qubits=range(L), inplace=True)

mpf_circuits.append(comp_circuit)

Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique

import copy
from qiskit.transpiler import Target, CouplingMap

target = backend.target
instruction_2q = "cz"

cmap = target.build_coupling_map(filter_idle_qubits=True)
cmap_list = list(cmap.get_edges())

max_meas_err = 0.055
min_t2 = 30
max_twoq_err = 0.01

# Remove qubits with bad measurement or t2
cust_cmap_list = copy.deepcopy(cmap_list)
for q in range(target.num_qubits):
meas_err = target["measure"][(q,)].error
if target.qubit_properties[q].t2 is not None:
t2 = target.qubit_properties[q].t2 * 1e6
else:
t2 = 0
if meas_err > max_meas_err or t2 < min_t2:
# print(q)
for q_pair in cmap_list:
if q in q_pair:
try:
cust_cmap_list.remove(q_pair)
except ValueError:
continue

# Remove qubits with bad 2q gate or t2
for q in cmap_list:
twoq_gate_err = target[instruction_2q][q].error
if twoq_gate_err > max_twoq_err:
# print(q)
for q_pair in cmap_list:
if q == q_pair:
try:
cust_cmap_list.remove(q_pair)
except ValueError:
continue

cust_cmap = CouplingMap(cust_cmap_list)

cust_target = Target.from_configuration(
basis_gates=backend.configuration().basis_gates
+ ["measure"], # or whatever new set of gates
coupling_map=cust_cmap,
)

sorted_components = sorted(
[list(comp.physical_qubits) for comp in cust_cmap.connected_components()],
reverse=True,
)
print("size of largest component", len(sorted_components[0]))
size of largest component 73
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

transpiler = generate_preset_pass_manager(
optimization_level=3, target=cust_target
)

transpiled_circuits = [transpiler.run(circ) for circ in mpf_circuits]

qubits_layouts = [
[
idx
for idx, qb in circuit.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
for circuit in transpiled_circuits
]

transpiled_circuits = []
for circuit, layout in zip(mpf_circuits, qubits_layouts):
transpiler = generate_preset_pass_manager(
optimization_level=3, backend=backend, initial_layout=layout
)
transpiled_circuit = transpiler.run(circuit)
transpiled_circuits.append(transpiled_circuit)

# transform the observable defined on virtual qubits to
# an observable defined on all physical qubits
isa_observables = [
observable.apply_layout(circ.layout) for circ in transpiled_circuits
]

Étape 3 : Exécuter en utilisant les primitives Qiskit

from qiskit_ibm_runtime import EstimatorV2 as Estimator

estimator = Estimator(mode=backend)
estimator.options.default_shots = 30000

# Set simple error suppression/mitigation options
estimator.options.dynamical_decoupling.enable = True
estimator.options.twirling.enable_gates = True
estimator.options.twirling.enable_measure = True
estimator.options.twirling.num_randomizations = "auto"
estimator.options.twirling.strategy = "active-accum"
estimator.options.resilience.measure_mitigation = True
estimator.options.experimental.execution_path = "gen3-turbo"

estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 1.2, 1.4)
estimator.options.resilience.zne.extrapolator = "linear"

estimator.options.environment.job_tags = ["mpf large"]

job_50 = estimator.run(
[
(circ, observable)
for circ, observable in zip(transpiled_circuits, isa_observables)
]
)

Étape 4 : Post-traiter et retourner le résultat dans le format classique souhaité

result = job_50.result()
evs = [res.data.evs for res in result]
std = [res.data.stds for res in result]

print(evs)
print(std)
[array(-0.08034071), array(-0.00605026), array(-0.15345759), array(-0.18127293)]
[array(0.04482517), array(0.03438413), array(0.21540776), array(0.21520829)]
exact_mpf_std = np.sqrt(
sum([(coeff**2) * (std**2) for coeff, std in zip(mpf_coeffs, std[:3])])
)
print(
"Exact static MPF expectation value: ",
evs[:3] @ mpf_coeffs,
"+-",
exact_mpf_std,
)
approx_mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(coeffs_approx.value, std[:3])
]
)
)
print(
"Approximate static MPF expectation value: ",
evs[:3] @ coeffs_approx.value,
"+-",
approx_mpf_std,
)
dynamic_mpf_std = np.sqrt(
sum(
[
(coeff**2) * (std**2)
for coeff, std in zip(mpf_dynamic_coeffs, std[:3])
]
)
)
print(
"Dynamic MPF expectation value: ",
evs[:3] @ mpf_dynamic_coeffs,
"+-",
dynamic_mpf_std,
)
Exact static MPF expectation value:  -0.47510243192011536 +- 0.6613940032465087
Approximate static MPF expectation value: -0.20914170384216998 +- 0.32341567460419135
Dynamic MPF expectation value: -0.07994951978722761 +- 0.07423091963310202
sym = {2: "^", 3: "s", 4: "p"}
# Get expectation values at all times for each Trotter step
for k, step in enumerate(mpf_trotter_steps):
plt.errorbar(
k,
evs[k],
yerr=std[k],
alpha=0.5,
markersize=4,
marker=sym[step],
color="grey",
label=f"{mpf_trotter_steps[k]} Trotter steps",
)

plt.errorbar(
3,
evs[-1],
yerr=std[-1],
alpha=0.5,
markersize=8,
marker="x",
color="blue",
label="6 Trotter steps",
)

plt.errorbar(
4,
evs[:3] @ mpf_coeffs,
yerr=exact_mpf_std,
markersize=4,
marker="o",
color="purple",
label="Static MPF",
)

plt.errorbar(
5,
evs[:3] @ coeffs_approx.value,
yerr=approx_mpf_std,
markersize=4,
marker="o",
color="orange",
label="Approximate static MPF",
)

plt.errorbar(
6,
evs[:3] @ mpf_dynamic_coeffs,
yerr=dynamic_mpf_std,
markersize=4,
marker="o",
color="pink",
label="Dynamic MPF",
)

exact_obs = -0.24384471447172074 # Calculated via Tensor Network calculation
plt.axhline(
y=exact_obs, linestyle="--", color="red", label="Exact time-evolution"
)

plt.title(
f"Expectation values for (ZZ,{(L//2-1, L//2)}) at time {total_time} for the different methods "
)
plt.xlabel("Method")
plt.ylabel("Expectation Value")
plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.2), ncol=2)
plt.grid(alpha=0.1)
plt.tight_layout()
plt.show()

Output of the previous code cell

Lors de l'exécution de circuits sur du matériel, nous pouvons rencontrer des défis supplémentaires pour obtenir des valeurs d'espérance précises en raison de la présence de bruit matériel. Ceci n'est pas pris en compte dans le formalisme MPF et pourrait jouer contre la solution MPF. Par exemple, cela pourrait être la raison de l'échec des coefficients dynamiques à fournir une meilleure estimation de la valeur d'espérance par rapport au coefficient statique approximatif dans le graphique. C'est-à-dire que l'évolueur approximatif, qui simule le circuit approximatif, ne reflète pas avec précision les résultats obtenus en exécutant les circuits approximatifs en présence de bruit matériel. Pour ces raisons, il est recommandé de combiner différentes techniques d'atténuation d'erreur afin d'obtenir des résultats aussi proches que possible des valeurs idéales pour chacune des formules produit. Cela montrera des avantages cohérents de l'approche MPF.

Dans l'ensemble, les coefficients statiques approximatifs donnent toujours une solution plus précise que la formule produit avec un nombre supérieur d'étapes de Trotter avec la même quantité d'erreur de Trotter dans le contexte sans bruit.

Il est également important de noter que dans l'exemple qui reproduit l'expérience de la Réf. [3], le point temporel t=3t=3 est au-delà de la limite à laquelle on s'attend à ce que la PF avec k=2k=2 se comporte bien, qui est t/k>1t/k>1 comme discuté dans ce guide.

Références

[1] Vazquez, A. C., Egger, D. J., Ochsner, D., & Woerner, S. (2023). Well-conditioned multi-product formulas for hardware-friendly Hamiltonian simulation. Quantum, 7, 1067.

[2] Zhuk, S., Robertson, N. F., & Bravyi, S. (2024). Trotter error bounds and dynamic multi-product formulas for Hamiltonian simulation. Physical Review Research, 6(3), 033309.