ASME Dayton Engineering Sciences Symposium 2017
Joseph C. Slater, October 23, 2017
A wide array of contemporary problems can be represented by nonlinear ordinary differential equations with solutions that can be represented by Fourier Series:
These can all be observed in a quick literature search on 'Harmonic Balance'.
eig
eig
only provides simple access to high-end eigensolvers written in C
and Fortran
Mousai
, an undergraduate can solve a nonlinear harmonic response problem easier then a PhD can today.This is the common State-Space form: -solutions exceedingly well known if it is linear
Finding the oscillatory response, after dissipation of the transient response, requires long time marching.
For a linear system in the frequency domain this is \begin{equation} j\omega\mathbf{Z}(\omega)=\mathbf{f}(\mathbf{Z}(\omega),\mathbf{U}(\omega)) \end{equation} \begin{equation} j\omega\mathbf{Z}(\omega)=A\mathbf{Z}(\omega)+B\mathbf{U}(\omega) \end{equation} where \begin{equation} A = \frac{\partial \mathbf{f}(\mathbf{Z}(\omega),\mathbf{U}(\omega))}{\partial\mathbf{Z}(\omega)},\qquad B = \frac{\partial \mathbf{f}(\mathbf{Z}(\omega),\mathbf{U}(\omega))}{\partial\mathbf{U}(\omega)} \end{equation} are constant matrices.
The solution is: \begin{equation} \mathbf{Z}(\omega) = \left(Ij\omega-A\right)^{-1}B\mathbf{U}(\omega) \end{equation} Where the magnitudes and phases of the elements of $\mathbf{Z}$ provide the amplitudes and phases of the harmonic response of each state at the frequency $\omega$.
For a nonlinear system in the frequency domain we assume a Fourier series solution \begin{equation} \mathbf{z}(t)=\lim_{N\to\infty}\sum_{n=-N}^{N}\mathbf{Z}_n e^{j n \omega t} \end{equation}
$N=1$ for a single harmonic. $n=0$ is the constant term.
This is actually a function call to a Finite Element Package, CFD, Matlab function, - whatever your solver uses to get derivatives
We can also find $\dot{\mathbf{z}}(t)$ from the derivative of the Fourier Series: \begin{equation} \dot{\mathbf{z}}(t)=\lim_{N\to\infty}\sum_{n=-N}^{N}j n \omega\mathbf{Z}_n e^{j n \omega t} \end{equation}
\begin{equation} \mathbf{0} \approx\sum_{n=-N}^{N}j n\omega \mathbf{Z}_n e^{j n \omega t}-\mathbf{f}\left(\sum_{n=-N}^{N}\mathbf{Z}_n e^{j n \omega t},\mathbf{u}(t)\right) \end{equation}
$$\ddot{x}+0.1\dot{x}+x+0.1 x^3=\sin(\omega t)$$
# Define our function (Python)
def duff_osc_ss(x, params):
omega = params['omega']
t = params['cur_time']
xd = np.array([[x[1]],
[-x[0] - 0.1 * x[0]**3 - 0.1 * x[1] + 1 * sin(omega * t)]])
return xd
# Arguments are name of derivative function, number of states, driving frequency,
# form of the equation, and number of harmonics
t, x, e, amps, phases = ms.hb_so(duff_osc_ss, num_variables=2, omega=.1,
eqform='first_order', num_harmonics=5)
print('Displacement amplitude is ', amps[0])
print('Velocity amplitude is ', amps[1])
Displacement amplitude is 0.946956354668 Velocity amplitude is 0.0946956354558
time, xc = ms.time_history(t, x)
pltcont()# abbreviated plotting function
omegal = np.arange(3, .03, -1 / 200) + 1 / 200
ampl = sp.zeros_like(omegal)
ampl[:] = np.nan
t, x, e, amps, phases = ms.hb_so(duff_osc_ss, num_variables=2,
omega=3, eqform='first_order', num_harmonics=1)
for i, freq in enumerate(omegal):
# Here we try to obtain solutions, but if they don't work,
# we ignore them by inserting `np.nan` values.
x = x - np.average(x)
try:
t, x, e, amps, phases =
ms.hb_so(duff_osc_ss, x0=x,
omega=freq, eqform='first_order', num_harmonics=1)
ampl[i] = amps[0]
except:
ampl[i] = np.nan
if np.isnan(ampl[i]):
break
plt.plot(omega,amp, label='Up sweep')
plt.plot(omegal,ampl, label='Down sweep')
plt.legend()
plt.title('Amplitude versus frequency for Duffing Oscillator')
plt.xlabel('Driving frequency $\\omega$')
plt.ylabel('Amplitude')
plt.grid()
$ \begin{bmatrix} 1&0\\ 0&1 \end{bmatrix} \begin{bmatrix} \ddot{x}_1\\ \ddot{x}_2\end{bmatrix} + \begin{bmatrix} 2&-1 \\ -1&2 \end{bmatrix} \begin{bmatrix} {x}_1\\{x}_2\end{bmatrix} +$ $ \begin{bmatrix} \alpha x_{1}^{3}\\ 0 \end{bmatrix} $ $= \begin{bmatrix} 0 \\ A \sin(\omega t) \end{bmatrix} $
def two_dof_demo(x, params):
omega = params['omega']
t = params['cur_time']
force_amplitude = params['force_amplitude']
alpha = params['alpha']
# The following could call an external code to obtain the state derivatives
xd = np.array([[x[1]],
[-2 * x[0] - alpha * x[0]**3 + x[2]],
[x[3]],
[-2 * x[2] + x[0]]] + force_amplitude * np.sin(omega * t))
return xd
parameters = {'force_amplitude': 0.2}
parameters['alpha'] = 0.4
t, x, e, amps, phases = ms.hb_so(two_dof_demo, num_variables=4,
omega=1.2, eqform='first_order', params=parameters)
amps
array([ 0.86696762, 0.89484597, 0.99030411, 1.04097851])
alpha = np.linspace(-1, .45, 2000)
amp = np.zeros_like(alpha)
for i, alphai in enumerate(alpha):
parameters['alpha'] = alphai
t, x, e, amps, phases = ms.hb_so(two_dof_demo, num_variables=4, omega=1.2,
eqform='first_order', params=parameters)
amp[i] = amps[0]
plt.plot(alpha,amp)
plt.title('Amplitude of $x_1$ versus $\\alpha$')
plt.ylabel('Amplitude of $x_1$')
plt.xlabel('$\\alpha$')
plt.grid()
$ \begin{bmatrix} 1&0\\ 0&1 \end{bmatrix} \begin{bmatrix} \ddot{x}_1\\ \ddot{x}_2\end{bmatrix} + \begin{bmatrix} 2&-1 \\ -1&2 \end{bmatrix} \begin{bmatrix} {x}_1\\{x}_2\end{bmatrix} +$ $ \begin{bmatrix} \mu |\dot{x}|_{1}\\ 0 \end{bmatrix} $ $= \begin{bmatrix} 0 \\ A \sin(\omega t) \end{bmatrix} $
def two_dof_coulomb(x, params):
omega = params['omega']
t = params['cur_time']
force_amplitude = params['force_amplitude']
mu = params['mu']
# The following could call an external code to obtain the state derivatives
xd = np.array([[x[1]],
[-2 * x[0] - mu * np.abs(x[1]) + x[2]],
[x[3]],
[-2 * x[2] + x[0]]] + force_amplitude * np.sin(omega * t))
return xd
mu = np.linspace(0, 1.0, 200)
amp = np.zeros_like(mu)
for i, mui in enumerate(mu):
parameters['mu'] = mui
t, x, e, amps, phases = ms.hb_so(two_dof_coulomb, num_variables=4, omega=1.2,
eqform='first_order', num_harmonics=3, params=parameters)
amp[i] = amps[0]
plt.plot(mu,amp)
plt.title('Amplitude of $x_1$ versus $\\mu$')
plt.ylabel('Amplitude of $x_1$')
plt.xlabel('$\\mu$')
plt.grid()
Damped Duffing oscillator in one command.
out = ms.hb_so(lambda x, v,
params: np.array([[-x - .1 * x**3 - .1 * v + 1 *
sin(params['omega'] * params['cur_time'])]]),
num_variables=1, omega=.7, num_harmonics=1)
out[3][0]
1.4779630014433971
OK - that's a bit obtuse. I wouldn't do that normally, but Mousai can.
pip install mousai