We first consider a general non-linear dynamical system, which is representative of many engineering problems:
$$ \dot{\vx} = \vf(\vx,t,\mu),\quad \vx(0)=\vx_0 $$with (discrete-time) measurements:
$$ \vy_k=\vg(\vx_k) $$Why interested in the dynamics:
We first consider a simple spatially-1D example consisting of
titles = ['$f_1(x,t)$', '$f_2(x,t)$', '$f$']
data = [X1, X2, X]
_=pltContours(xgrid, tgrid, data, titles)
Question:
Is it possible to ...
First let's try PCA.
titles = ['Data', 'PCA Reconstruction', 'Residual']
data = [X, X_pca, X-X_pca]
_=pltContours(xgrid, tgrid, data, titles)
_=pltMD(mod_pca, dyn_pca.T, 'PCA') # Non-exact modes, non-exact periods
Issues with PCA:
Dynamic Mode Decomposition to the rescue.
In DMD, we approximate the non-linear dynamical system by a low dimensional linear system: $$ \dot{\vx}=\bar{\vA}\vx $$ where $\bar{\vA}$ has eigenvalues $\bar{\lambda}=\zeta+\omega i$ and eigenvectors $\vtf$.
Then the solution is determined from the initial condition $\vx(0)=\vx_0$, $$ \vx=\sum_{i=1}^n b_i \vtf_i e^{\bar{\lambda}_i t} $$ where $b_i=\vtf_i^T\vx_0$
But typically the state measurement occurs at discrete time $\vx_j=\vx(t_j)$, with a time step size $\Delta t$, so one has a discrete linear system, $$ \vx_{k+1} = \vA \vx_k $$ where $\vA=\exp(\vA\Delta t)$, and $\vA$ has the same eigenvectors $\vtf$ as $\bar{\vA}$, but the eigenvalues become, $$ \lambda = \exp(\bar{\lambda}\Delta t),\quad \mathrm{or},\quad \bar{\lambda} = \frac{\log|\lambda|}{\Delta t} + \frac{\mathrm{Arg}(\lambda)}{\Delta t} i $$ And the solution given $\vx(0)=\vx_0$ is now written as, $$ \vx_{k+1}=\sum_{i=1}^n b_i \lambda_i^{k+1} \vtf_i $$
The eigenvalues determine the stability of the modes, i.e. whether the response decays or grows in time. Specifically, for the continuous case, we check $\zeta=Re(\bar{\lambda})$; for the discrete case, we check $|\lambda|$:
The eigenvalues: $\lambda_{1,2}=\frac{1}{2}\left( p\pm\sqrt\Delta \right)$
Be careful with the boundaries - what happens when
We shall explore more later in nonmodal analysis (if time permits)
Now assume full-state measurement: $\vx_j=\vx(t_j)$
Assemble $m$ measurements into the following:
$$ \begin{align} \vX &= [\vx_1 | \vx_2 | \cdots | \vx_{m-1}] \\ \vX^1 &= [\vx_2 | \vx_3 | \cdots | \vx_m] \end{align} $$If the system is linear, there is a discrete-time system matrix satisfying $$ \vX^1 = \vA\vX $$
This is a least squares problem, and the solution is $$ \vA = \vX^1 \vX^+ $$ where $\vX^+$ is the pseudo-inverse.
Again, be careful this $\vA$ is different from the $\bar{\vA}$ in the continuous system.
To find $\vA$, and the associated modes and eigenvalues, we carry out the following steps:
Step 1: Rank-r truncation $$ \vX=\vU_r\vtS_r\vV_r^* $$
Step 2: System matrix $$ \vA = \vX^1\vV_r\vtS_r^{-1}\vU_r^* \equiv \vB\vU_r^* $$ and the reduced system matrix $$ \tilde\vA = \vU_r^*\vA\vU_r = \vU_r^*\vB $$
Step 3: Reduced eigenvalue problem $$ \tilde\vA\vw = \lambda\vw $$
Step 4: Eigensystem recovery
There are two ways to find the modes $\vtf_i$ associated with the original system. One is approximate, and the other exact.
Comparing the two modes: $$ \vU_r\vU_r^*\vtf = \frac{1}{\lambda}\vU_r\vU_r^*\vB\vw = \frac{1}{\lambda}\vU_r\tilde\vA\vw = \vU_r\vw = \hat{\vtf} $$ So $\hat{\vtf}$ is the projection of $\vtf$ onto the $\vU_r$ space. Potential information loss due to projection!
Finally, the eigenvalues $\lambda$ of $\vA$ (discrete system) can be converted back to $\bar{\lambda}$ of $\bar{\vA}$ (continuous system) to identify the damping and frequency associated with the DMD modes $\vtf$.
In terms of system identification, there are classical algorithms like the Eigensystem Realization Algorithm (ERA) and the Observer Kalman Filter Identification (OKID).
Another commonly used approach is from signal processing, called Empirical Mode Decomposition (EMD), or., e.g., its more recent variant, Variational Mode Decomposition (VMD).
f, ax = plt.subplots(ncols=2, figsize=(12,5))
ax[0].plot(x, dmd.modes[:,0].real, 'b-', label='DMD'); ax[0].plot(x, dmd.modes[:,1].real, 'r-')
ax[0].plot(x, -M1.real, 'k:', label='Exact'); ax[0].plot(x, M2.real, 'k:')
ax[0].legend(); ax[0].set_xlabel('x'); ax[0].set_ylabel('Mode shape'); ax[0].set_title('Modes')
ax[1].plot(t, dmd.dynamics[0].real, 'b-', label='DMD'); ax[1].plot(t, dmd.dynamics[1].real, 'r-')
ax[1].plot(t, np.real(D2), 'k:', label='Exact'); ax[1].plot(t, np.real(-D1), 'k:')
ax[1].legend(); ax[1].set_xlabel('t'); ax[1].set_ylabel('Amplitude'); _=ax[1].set_title('Dynamics')
titles = ['Mode 1', 'Mode 2', 'Reconstruction']
data = [dmd.dynamics[0].reshape(-1, 1).dot(dmd.modes[:,0].reshape(1, -1)),
dmd.dynamics[1].reshape(-1, 1).dot(dmd.modes[:,1].reshape(1, -1)),
dmd.reconstructed_data.T]
_=pltContours(xgrid, tgrid, data, titles)
titles = ['Residual', 'White noise', 'Uncaptured mode components']
data = [(X-dmd.reconstructed_data.T).real,
Xn,
(X-dmd.reconstructed_data.T-Xn).real]
_=pltContours(xgrid, tgrid, data, titles)
We applied DMD to complex data, but only check the real part of reconstructed data.
f = plt.figure(figsize=(6,4))
plt.plot(x, dmd_real.modes[:,0], 'b--',
label='Real only: {0:5.4f}/{1:5.4f}'.format(*_eig(dmd_real.eigs)))
plt.plot(x, dmd_real.modes[:,1], 'r--')
plt.plot(x, M1.real, 'k:',
label='Exact: {0:5.4f}/{1:5.4f}'.format(w2, w1))
plt.plot(x, M2.real, 'k:')
plt.legend()
plt.xlabel('x')
_=plt.ylabel('Mode shape')
The real-only DMD failed like PCA, but for a different reason.
f = plt.figure(figsize=(12,4))
plt.plot(x, _nrm(dmd.modes[:,0].real), 'b-.', label='Complex: {0:5.4f}/{1:5.4f}'.format(*_eig(dmd.eigs)))
plt.plot(x, -_nrm(dmd.modes[:,1].real), 'r-.')
plt.plot(x, -_nrm(dmd_expd.modes[:128,0]), 'b-', label='Expanded: {0:5.4f}/{2:5.4f}'.format(*_eig(dmd_expd.eigs)))
plt.plot(x, _nrm(dmd_expd.modes[:128,2]), 'r-')
plt.plot(x, -_nrm(dmd_tde.modes[:128,0]), 'b.', label='TDE: {0:5.4f}/{2:5.4f}'.format(*_eig(dmd_tde.eigs)))
plt.plot(x, -_nrm(dmd_tde.modes[:128,2]), 'r.')
plt.plot(x, dmd_real.modes[:,0], 'b--', label='Real only: {0:5.4f}/{1:5.4f}'.format(*_eig(dmd_real.eigs)))
plt.plot(x, dmd_real.modes[:,1], 'r--')
plt.plot(x, M1.real, 'k:', label='Exact: {0:5.4f}/{1:5.4f}'.format(w2, w1))
plt.plot(x, M2.real, 'k:')
plt.legend()
plt.xlabel('x')
_=plt.ylabel('Mode shape')
More discussion on DMD, and its generalization Koopman theory, in the Dynamical Systems module.
A relatively comprehensive set of DMD, from PyDMD.