Skip to content

Linear response

In HarmonicBalance.jl, the stability and linear response are treated using the LinearResponse module.

Here we calculate the white noise response of a simple nonlinear system. A set of reference results may be found in Huber et al. in Phys. Rev. X 10, 021066 (2020). We start by defining the Duffing oscillator

julia
using HarmonicBalance, Plots
using Plots.Measures: mm
@variables α, ω, ω0, F, γ, t, x(t); # declare constant variables and a function x(t)

# define ODE
diff_eq = DifferentialEquation(d(x,t,2) + ω0*x + α*x^3 + γ*d(x,t) ~ F*cos*t), x)

# specify the ansatz x = u(T) cos(ω*t) + v(T) sin(ω*t)
add_harmonic!(diff_eq, x, ω)

# implement ansatz to get harmonic equations
harmonic_eq = get_harmonic_equations(diff_eq)
A set of 2 harmonic equations
Variables: u1(T), v1(T)
Parameters: ω, α, γ, ω0, F

Harmonic ansatz: 
x(t) = u1(T)*cos(ωt) + v1(T)*sin(ωt)

Harmonic equations:

u1(T)*ω0 + (2//1)*Differential(T)(v1(T))*ω + Differential(T)(u1(T))*γ - u1(T)*(ω^2) + v1(T)*γ*ω + (3//4)*(u1(T)^3)*α + (3//4)*u1(T)*(v1(T)^2)*α ~ F

Differential(T)(v1(T))*γ + v1(T)*ω0 - (2//1)*Differential(T)(u1(T))*ω - u1(T)*γ*ω - v1(T)*(ω^2) + (3//4)*(u1(T)^2)*v1(T)*α + (3//4)*(v1(T)^3)*α ~ 0//1

Linear regime

When driven weakly, the Duffing resonator behaves quasi-linearly, i.e, its response to noise is independent of the applied drive. We see that for weak driving, F=104, the amplitude is a Lorentzian.

julia
fixed ==> 1, ω0 => 1.0, γ => 0.005, F => 0.0001)   # fixed parameters
varied = ω => range(0.95, 1.05, 100)           # range of parameter values
result = get_steady_states(harmonic_eq, varied, fixed)

plot(result, "sqrt(u1^2 + v1^2)")

To find the fluctuation on the top of the steady state one often employs a Bogoliubov-de Gennes analyses. Here, we compute the eigenvalues λk of the Jacobian matrix at the steady state. The imaginary part of the eigenvalues gives characteristic frequencies of the "quasi-particle excitations". The real part gives the lifetime of these excitations.

One can plot the eigenvalues as follows

julia
plot(
    plot_eigenvalues(result, branch=1),
    plot_eigenvalues(result, branch=1, type=:real, ylims=(-0.003, 0)),
)

We find a single pair of complex conjugate eigenvalues linearly changing with the driving frequency. Both real parts are negative, indicating stability.

As discussed in background section on linear response, the excitation manifest itself as a lorentenzian peak in a power spectral density (PSD) measurement. The PSD can be plotted using plot_linear_response:

julia
plot_linear_response(result, x, Ω_range=range(0.95, 1.05, 300), branch=1, logscale=true)

The response has a peak at ω0, irrespective of the driving frequency ω. Indeed, the eigenvalues shown before where plotted in the rotating frame at the frequency of the drive ω. Hence, the imaginary part of eigenvalues shows the frequency (energy) needed to excite the system at it natural frequency (The frequency its want to be excited at.)

Note the slight "bending" of the noise peak with ω - this is given by the failure of the first-order calculation to capture response far-detuned from the drive frequency.

Nonlinear regime

For strong driving, matters get more complicated. Let us now use a drive F=2103 :

julia
fixed ==> 1, ω0 => 1.0, γ => 0.005, F => 0.002)   # fixed parameters
varied = ω => range(0.95, 1.05, 100)           # range of parameter values
result = get_steady_states(harmonic_eq, varied, fixed)

plot(result, x="ω", y="sqrt(u1^2 + v1^2)");

The amplitude is the well-known Duffing curve. Let's look at the eigenvalues of the two stable branches, 1 and 2.

julia
plot(
    plot_eigenvalues(result, branch=1),
    plot_eigenvalues(result, branch=1, type=:real, ylims=(-0.003, 0)),
    plot_eigenvalues(result, branch=2),
    plot_eigenvalues(result, branch=2, type=:real, ylims=(-0.003, 0)),
)

Again every branch gives a single pair of complex conjugate eigenvalues. However, for branch 1, the characteristic frequencies due not change linearly with the driving frequency around ω=ω0. This is a sign of steady state becoming nonlinear at large amplitudes.

The same can be seen in the PSD:

julia
plot(
  plot_linear_response(result, x, branch=1, Ω_range=range(0.95,1.1,300), logscale=true),
  plot_linear_response(result, x, branch=2, Ω_range=range(0.9,1.1,300), logscale=true),
    size=(600, 250), margin=3mm
)

In branch 1 the linear response to white noise shows more than one peak. This is a distinctly nonlinear phenomenon, indicative if the squeezing of the steady state. Branch 2 is again quasi-linear, which stems from its low amplitude.

Following Huber et al., we may also fix ω=ω0 and plot the linear response as a function of F. The response turns out to be single-valued over a large range of driving strengths. Using a log scale for the x-axis:

julia
fixed ==> 1., ω0 => 1.0, γ => 1e-2, ω => 1)   # fixed parameters
swept = F => 10 .^ range(-6, -1, 200)           # range of parameter values
result = get_steady_states(harmonic_eq, swept, fixed)

plot(
  plot(result, "sqrt(u1^2 + v1^2)", xscale=:log),
  plot_linear_response(result, x, branch=1, Ω_range=range(0.9,1.1,300), logscale=true, xscale=:log),
  size=(600, 250), margin=3mm
)

We see that for low F, quasi-linear behaviour with a single Lorentzian response occurs, while for larger F, two peaks form in the noise response. The two peaks are strongly unequal in magnitude, which is an example of internal squeezing (See supplemental material of Huber et al.).