Skip to content

Finding the staedy states of a Duffing oscillator

Here we show the workflow of HarmonicBalance.jl on a simple example - the driven Duffing oscillator. The equation of motion for the displacement x(t) reads

x¨(t)+γx˙(t)+ω02x(t)damped harmonic oscillator+αx(t)3Duffing coefficient=Fcos(ωt)periodic drive

In general, there is no analytical solution to the differential equation. Fortunately, some harmonics are more important than others. By truncating the infinite-dimensional Fourier space to a set of judiciously chosen harmonics, we may obtain a soluble system. For the Duffing resonator, we can well try to only consider the drive frequency ω. To implement this, we use the harmonic ansatz

x(t)=Ucos(ωt)+Vsin(ωt),

which constraints the spectrum of x(t) to a single harmonic. Fixing the quadratures U and V to be constant then reduces the differential equation to two coupled cubic polynomial equations (for more details on this step, see the appendices in the white paper). Finding the roots of coupled polynomials is in general very hard. We here apply the method of homotopy continuation, as implemented in HomotopyContinuation.jl which is guaranteed to find the complete set of roots.

First we need to declare the symbolic variables (the excellent Symbolics.jl is used here).

julia
using HarmonicBalance
@variables α ω ω0 F γ t x(t) # declare constant variables and a function x(t)

Next, we have to input the equations of motion. This will be stored as a DifferentialEquation. The input needs to specify that only x is a mathematical variable, the other symbols are parameters:

julia
diff_eq = DifferentialEquation(d(x,t,2) + ω0^2*x + α*x^3 + γ*d(x,t) ~ F*cos*t), x)
System of 1 differential equations
Variables:       x(t)
Harmonic ansatz: x(t) => ;   

Differential(t)(Differential(t)(x(t))) + Differential(t)(x(t))*γ + x(t)*(ω0^2) + (x(t)^3)*α ~ F*cos(t*ω)

One harmonic

The harmonic ansatz needs to be specified now – we expand x in a single frequency ω.

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

The object diff_eq now contains all the necessary information to convert the differential equation to the algebraic harmonic equations (coupled polynomials in U and V).

julia
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:

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

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

The variables u1 and v1 were declared automatically to construct the harmonic ansatz. The slow time variable T describes variation of the quadratures on timescales much slower than ω. For a steady state, all derivatives w.r.t T vanish, leaving only algebraic equations to be solved.

We are ready to start plugging in numbers! Let us find steady states by solving harmonic_eq for numerical parameters. Homotopy continuation is especially suited to solving over a range of parameter values. Here we will solve over a range of driving frequencies ω – these are stored as Pairs{Sym, Vector{Float64}}:

julia
varied = ω => range(0.9, 1.2, 100); # range of parameter values
ω => 0.9:0.0030303030303030303:1.2

The other parameters we be fixed – these are declared as Pairs{Sym, Float64} pairs:

julia
fixed ==> 1., ω0 => 1.0, F => 0.01, γ => 0.01); # fixed parameters
(α => 1.0, ω0 => 1.0, F => 0.01, γ => 0.01)

Now everything is ready to crank the handle. get_steady_states solves our harmonic_eq using the varied and fixed parameters:

julia
result = get_steady_states(harmonic_eq, varied, fixed)
A steady state result for 100 parameter points

Solution branches:   3
   of which real:    3
   of which stable:  2

Classes: stable, physical, Hopf, binary_labels

The algorithm has found 3 solution branches in total (out of the hypothetically admissible 32=9). All of these are real – and thefore physically observable – for at least some values of ω. Only 2 branches are stable under infinitesimal perturbations. The "Classes" are boolean labels classifying each solution point, which may be used to select results for plotting.

We now want to visualize the results. Here we plot the solution amplitude, U2+V2 against the drive frequency ω:

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

This is the expected response curve for the Duffing equation.

Using multiple harmonics

In the above section, we truncated the Fourier space to a single harmonic ω – the oscillator was assumed to only oscillate at the drive frequency. However, the Duffing oscillator can exhibit a rich spectrum of harmonics. We can obtain some intuition by treating α perturbatively in the equation of motion, i.e., by solving

x¨(t)+γx˙(t)+ω02x(t)+ϵαx(t)3=Fcos(ωt)

for small ϵ. To zeroth order, the response of the system is x0(t)=X0cos(ωt+ϕ0). Expanding x(t)=x0(t)+ϵx1(t), we find that the perturbation x1(t) satisfies to first order

x¨1(t)+γx˙1(t)[ω02+3αX024]x1(t)=αX034cos(3ωt+3ϕ0),

which gives a response of the form x1(t)=X1cos(3ωt+ϕ1). Clearly, the oscillator now responds not only at frequency ω, but also at 3ω! This effect is known as high harmonic generation or more generally frequency conversion. By continuing the procedure to higher orders, we eventually obtain an infinity of harmonics present in the response. In general, there is no analytical solution to such problems.

We argued that frequency conversion takes place, to first order from ω to 3ω. We can reflect this process by using a extended harmonic ansatz:

x(t)=U1cos(ωt)+V1sin(ωt)+U2cos(3ωt)+V2sin(3ωt).

Note that this is not a perturbative treatment! The harmonics ω and 3ω are on the same footing here. This is implemented as

julia
add_harmonic!(diff_eq, x, [ω, 3ω]) # specify the two-harmonics ansatz
harmonic_eq = get_harmonic_equations(diff_eq)
A set of 4 harmonic equations
Variables: u1(T), v1(T), u2(T), v2(T)
Parameters: ω, ω0, γ, α, F

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

Harmonic equations:

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

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

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

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

The variables u1, v1 now encode ω and u2, v2 encode . We see this system is much harder to solve as we now have 4 harmonic variables, resulting in 4 coupled cubic equations. A maximum of 34=81 solutions may appear!

julia
result = get_steady_states(harmonic_eq, varied, fixed)
plot(result, "sqrt(u1^2 + v1^2)")

For the above parameters (where a perturbative treatment would have been reasonable), the principal response at ω looks rather similar, with a much smaller upconverted component appearing at 3ω:

julia
p1=plot(result, "sqrt(u1^2 + v1^2)", legend=false)
p2=plot(result, "sqrt(u2^2 + v2^2)")
plot(p1, p2)

The non-perturbative nature of the ansatz allows us to capture some behaviour which is not a mere extension of the usual single-harmonic Duffing response. Suppose we drive a strongly nonlinear resonator at frequency ωω0/3. Such a drive is far out of resonance, however, the upconverted harmonic 3ω=ω0 is not and may play an important role! Let us try this out:

julia
fixed ==> 10., ω0 => 3, F => 5, γ=>0.01)   # fixed parameters
varied = ω => range(0.9, 1.4, 100)           # range of parameter values
result = get_steady_states(harmonic_eq, varied, fixed)
A steady state result for 100 parameter points

Solution branches:   9
   of which real:    3
   of which stable:  2

Classes: stable, physical, Hopf, binary_labels

Although 9 branches were found in total, only 3 remain physical (real-valued). Let us visualise the amplitudes corresponding to the two harmonics, U12+V12 and U22+V22 :

julia
p1 = plot(result, "sqrt(u1^2 + v1^2)", legend=false)
p2 = plot(result, "sqrt(u2^2 + v2^2)")
plot(p1, p2)

The contributions of ω and 3ω are now comparable and the system shows some fairly complex behaviour! This demonstrates how an exact solution within an extended Fourier subspace goes beyond a perturbative treatment.