Recapitulando

SINDy resolve \(\dot X \approx \Theta(X)\Xi\) por regressão esparsa (STLSQ). Aqui, aplicamos a um sistema clássico: Lotka–Volterra.

\[ \dot x = a x - b\,xy \qquad \dot y = -c y + d\,xy \]

Gerando os Dados

import numpy as np
from scipy.integrate import solve_ivp

a, b, c, d = 1.1, 0.4, 0.4, 0.1
def lotka_volterra(t, z):
    x, y = z
    return [a*x - b*x*y, -c*y + d*x*y]

t_eval = np.linspace(0, 60, 3000)
sol = solve_ivp(lotka_volterra, (0,60), [10,5], t_eval=t_eval, rtol=1e-10, atol=1e-10)
X = sol.y.T
dX = np.gradient(X, t_eval[1]-t_eval[0], axis=0)

Biblioteca de Candidatos

Polinomial até grau 2 em \((x,y)\) — 6 candidatos, só 2 termos por equação são verdadeiros:

x, y = X[:,0], X[:,1]
Theta = np.column_stack([np.ones_like(x), x, y, x**2, x*y, y**2])
nomes = ["1","x","y","x^2","xy","y^2"]

Implementando STLSQ

def stlsq(Theta, dX, thresh=0.05, n_iter=10):
    Xi = np.linalg.lstsq(Theta, dX, rcond=None)[0]
    for _ in range(n_iter):
        small = np.abs(Xi) < thresh
        Xi[small] = 0
        for c in range(dX.shape[1]):
            big = ~small[:,c]
            Xi[big,c] = np.linalg.lstsq(Theta[:,big], dX[:,c], rcond=None)[0]
    return Xi

Xi = stlsq(Theta, dX)

Resultado — Coeficientes

for nome, linha in zip(nomes, Xi):
    print(f"{nome:>4}: dx/dt={linha[0]:+.3f}  dy/dt={linha[1]:+.3f}")
   1: dx/dt=+0.000  dy/dt=+0.000
   x: dx/dt=+1.100  dy/dt=+0.000
   y: dx/dt=+0.000  dy/dt=-0.400
 x^2: dx/dt=+0.000  dy/dt=+0.000
  xy: dx/dt=-0.400  dy/dt=+0.100
 y^2: dx/dt=+0.000  dy/dt=+0.000
print(f"verdadeiro: a={a} b={-b} c={-c} d={d}")
verdadeiro: a=1.1 b=-0.4 c=-0.4 d=0.1

STLSQ zera os 4 termos espúrios e recupera \(a,-b,-c,d\) com 3 casas de precisão — só com np.gradient, sem PySINDy.

Resultado — Reconstrução

import matplotlib.pyplot as plt

def modelo(t, z):
    x, y = z
    th = np.array([1, x, y, x**2, x*y, y**2])
    return [th @ Xi[:,0], th @ Xi[:,1]]

sol2 = solve_ivp(modelo, (0,60), [10,5], t_eval=t_eval, rtol=1e-8, atol=1e-8)
fig, ax = plt.subplots()
_ = ax.plot(sol.y[0], sol.y[1], "-", lw=2, label="verdadeiro")
_ = ax.plot(sol2.y[0], sol2.y[1], "--", lw=2, label="descoberto")
_ = ax.legend()
plt.tight_layout(); plt.show()

O Que Pode Dar Errado

  • Ruído amplifica o erro de np.gradient — usar filtros ou diferenciação robusta antes do STLSQ;
  • \(\tau\) mal ajustado: descarta termos certos ou mantém espúrios;
  • Se \(\Theta\) não contiver o termo certo, nenhum \(\tau\) resolve — é especificação, não otimização (como a escolha de observáveis no Koopman).

Para dados ruidosos: Ensemble-SINDy (Fasel et al. 2022) e SR3 (Zheng et al. 2019).

Referências

Fasel, Urban, J. Nathan Kutz, Bingni W. Brunton, e Steven L. Brunton. 2022. «Ensemble-SINDy: Robust Sparse Model Discovery in the Low-Data, High-Noise Limit, with Active Learning and Control». Proceedings of the Royal Society A 478: 20210904. https://doi.org/10.1098/rspa.2021.0904.
Zheng, Peng, Travis Askham, Steven L. Brunton, J. Nathan Kutz, e Aleksandr Y. Aravkin. 2019. «A Unified Framework for Sparse Relaxed Regularized Regression: SR3». IEEE Access 7: 1404–23. https://doi.org/10.1109/ACCESS.2018.2886528.