Skip to content

3. Gradiente VQE Differenze Finite vs PSR Esatta

tatopenn-cell edited this page Jul 25, 2026 · 3 revisions

3. Gradiente VQE: Differenze Finite vs Parameter-Shift Rule Esatta

Due script calcolano lo stesso gradiente $dE/d\theta$ sullo stesso ansatz (catena di 6/12 qubit, blocchi CX–RY($\theta$)–CX–RY($\theta_B$)–CX per legame, hopping tight-binding $t=2.11\text{ eV}$), con due metodi diversi:

  • vqe_gradient.py — differenze finite classiche, $h=10^{-5}$, un singolo circuito per volta.
  • vqe_jax_grad.py / tests/test_vqe_jax_gradient.py — Parameter-Shift Rule, batchata su JAX.

Questo capitolo racconta anche un bug reale trovato e corretto durante l'audit su dense-evolution 8.1.21, perché capirlo è essenziale per non ripeterlo altrove.


Il bug: run_parametric_batch_jit conta gli slot in ordine di apparizione

DenseSVSimulator.run_parametric_batch_jit(base_circuit, parameter_batch) assegna una colonna di parameter_batch a ogni gate rx/ry/rz/p, in ordine di apparizione nel circuito — anche se l'argomento passato è un float letterale, non solo per i placeholder stringa. Il letterale viene ignorato.

L'ansatz di vqe_jax_grad.py ha, a $N_Q=6$, 5 legami $\times$ 2 gate ry = 10 slot parametrici. La griglia originale, però, aveva solo 2 colonne fisse [theta, -theta]. Risultato: solo il primo legame riceveva i valori giusti; gli altri 9 slot finivano fuori indice, e JAX — che non solleva errore su un accesso array fuori range, ma clippa silenziosamente all'ultimo indice valido — restituiva lo stesso valore clippato per tutti loro invece dell'angolo corretto.

Verificato empiricamente: costruendo la stessa griglia bacata e confrontandola con un circuito singolo dove OGNI legame riceve esplicitamente l'angolo corretto, la differenza sull'energia era enorme (fino a $\Delta E \approx 12$ su una scala tipica di pochi eV) — non rumore numerico, un circuito fisicamente diverso.

Il secondo problema: la PSR "condivisa" non è esatta

Anche dopo aver corretto il conteggio delle colonne, restava un problema più sottile: la formula usata,

$$\frac{dE}{dt} \approx \frac{1}{2}\left[E(t+\tfrac{\pi}{2}) - E(t-\tfrac{\pi}{2})\right]$$

shiftava la variabile condivisa $t$ (con $\theta_A = t$ su un gate, $\theta_B = -t$ o $-t/2$ su un altro), non il parametro di un singolo gate. La Parameter-Shift Rule standard è esatta solo quando si shifta il parametro di un solo gate, tenendo tutti gli altri fissi — è quello che verifica tests/test_analytical.py::test_psr_exactness_ry_z su un singolo $RY(\theta)|0\rangle + \langle Z\rangle$, dove l'accordo con $-\sin(\theta)$ è esatto a $10^{-10}$.

Shiftare una variabile condivisa che guida più gate conflagra i contributi di ciascuno. Verificato: la formula "condivisa" poteva discordare dal vero gradiente anche del 100%, segno incluso.

Il fix: gradiente esatto via regola della catena

$$\frac{dE}{dt} = \sum_{q} \left[\frac{\partial E}{\partial\theta_{A,q}} \cdot \frac{d\theta_{A,q}}{dt} + \frac{\partial E}{\partial\theta_{B,q}} \cdot \frac{d\theta_{B,q}}{dt}\right]$$

dove ogni $\partial E/\partial\theta$ è una PSR esatta a singolo gate (shift di $\pm\pi/2$ su quel solo gate, tutti gli altri fermi al valore base), e $d\theta/dt$ sono i coefficienti noti della parametrizzazione ($d\theta_A/dt=1$, $d\theta_B/dt=-1$ o $-0.5$ a seconda dello script).

Verificato contro differenze finite indipendenti: accordo a $\sim 10^{-9}$ (limitato dalla precisione del riferimento FD, non dalla PSR).

Costo: per un ansatz a $N$ gate parametrici serve $4N+1$ righe batch per punto (1 base + 2 shift $\times$ $N$ gate) invece di 3 — molto più caro, ma è il prezzo reale di un gradiente esatto su un parametro condiviso, batchabile comunque in un'unica chiamata run_parametric_batch_jit per punto.

Cosa è cambiato nei numeri pubblicati

vqe_jax_gradient.csv/.png sono stati rigenerati: 73.500 tracce invece di 10.500, ~570s invece di 58s su CPU — la differenza di costo è genuina, non un rallentamento accidentale.

Copertura test

tests/test_vqe_jax_gradient.py::test_vqe_jax_batch_grid_has_one_column_per_rotation_slot blocca la regressione sul conteggio colonne; test_vqe_jax_exact_psr_gradient_matches_finite_difference (5 valori di $\theta$) verifica l'esattezza del gradiente a catena. vqe_gradient.py (il riferimento a differenze finite) è validato in tests/test_integration_smoke.py contro un circuito PennyLane indipendente, chiamando la funzione reale calcola_energia_vqe, non una copia.

Aggiornamento: il bug di run_parametric_batch_jit descritto sopra è stato segnalato a monte (Dense-Evolution#6) e corretto in dense-evolution 8.1.28 — ora solleva ValueError su un mismatch di colonne invece di clippare in silenzio. Il fix qui (griglia a 10 colonne) resta corretto e necessario indipendentemente dalla versione del pacchetto.


Forma chiusa: E(θ) senza simulare nessun circuito

vqe_gradient.py condivide lo stesso $\theta$ su tutti i 5 legami (invece degli angoli indipendenti per legame del capitolo 6), quindi far variare $\theta$ traccia una sola curva 1-dimensionale attraverso la varietà a singola eccitazione, non l'intero spazio. Quella curva ha una forma chiusa, con la stessa ricorsione a cascata scoperta nel capitolo 6:

$$c_0(\theta) = \cos^{N-1}(\theta), \qquad c_q(\theta) = \sin(\theta)\cos^{N-1-q}(\theta) \quad (q=1,\ldots,N-1)$$

La somma cinetica di calcola_energia_vqe è periodica ($q_{\text{next}} = (q+1) \bmod N$, tutti gli $N=6$ legami incluso il giro $5\to0$ — non i $N{-}1$ legami a catena aperta usati dagli script molecolari):

$$E(\theta) = -2,t_{hopping}\sum_{q=0}^{N-1} c_q(\theta), c_{(q+1)\bmod N}(\theta)$$

Trovato un bug di distrazione mio durante la derivazione, utile da ricordare: la prima verifica contro calcola_energia_vqe non tornava (differenza reale, non rumore) — la formula assumeva legami a catena aperta (5, come negli script molecolari) invece di periodici (6, con il giro). Confrontare sempre contro la funzione vera dello script prima di fidarsi di una derivazione a mano, anche quando "sembra" già verificata contro una propria ricostruzione del circuito.

Verificato esatto (precisione macchina, $\sim 10^{-15}$) contro calcola_energia_vqe su tutto lo sweep, inclusi i checkpoint stampati — es. $\theta=0.4471\text{ rad} \to E=-3.9489\text{ eV}$, gradiente esattamente $-8.440000$ a $\theta=0$senza simulare nessun circuito: energia_forma_chiusa() in scripts/vqe_gradient.py. tests/test_integration_smoke.py::test_vqe_gradient_closed_form_matches_real_circuit_exactly verifica l'identità su 7 punti lungo il range.


Capitolo 2 · Home · Capitolo 4 ▶