1+ #=
2+ This example demonstrates how to compute and plot describing functions in the Nyquist plane
3+ =#
4+ using ControlSystems
5+ using QuadGK
6+
7+ """
8+ compute_describing_function(f, A)
9+
10+ Numerically computes the describing function N(A) for a nonlinearity f(x)
11+ at input amplitude A. Returns a Complex number.
12+ """
13+ function compute_describing_function (f, A)
14+ # Fundamental sine component coefficient (b1)
15+ # b1 = (1/π) * ∫ [f(A*sin(θ)) * sin(θ)] dθ from 0 to 2π
16+ b1_integral, _ = quadgk (θ -> f (A * sin (θ)) * sin (θ), 0 , 2 π, atol= 1e-6 , rtol= 1e-6 )
17+ b1 = b1_integral / π
18+
19+ # Fundamental cosine component coefficient (a1)
20+ # a1 = (1/π) * ∫ [f(A*sin(θ)) * cos(θ)] dθ from 0 to 2π
21+ a1_integral, _ = quadgk (θ -> f (A * sin (θ)) * cos (θ), 0 , 2 π, atol= 1e-6 , rtol= 1e-6 )
22+ a1 = a1_integral / π
23+
24+ # N(A) = (b1 + j*a1) / A
25+ return Complex (b1, a1) / A
26+ end
27+
28+ """
29+ plot_negative_inverse_df(f; A_range=range(0.1, 10, length=100), label="Nonlinearity")
30+ Computes and plots -1/N(A) for a given function f over a range of amplitudes.
31+ """
32+ function plot_negative_inverse_df (f; A_range= range (0.1 , 10 , length= 100 ), label= " Nonlinearity" , fig= nothing )
33+ # Compute the negative inverse values
34+ vals = similar (A_range, ComplexF64)
35+ Threads. @threads for i = eachindex (A_range)
36+ A = A_range[i]
37+ vals[i] = - 1.0 / compute_describing_function (f, A)
38+ end
39+
40+ # Extract real and imaginary parts for plotting
41+ re_vals = real .(vals)
42+ im_vals = imag .(vals)
43+
44+ # Create plot or add to existing one
45+ if fig === nothing
46+ fig = plot (title= " Negative Inverse Describing Function Analysis" ,
47+ xlabel= " Real" , ylabel= " Imaginary" , grid= true , zeroline= true )
48+ end
49+
50+ # Plot the curve with arrows to show increasing amplitude direction
51+ plot! (fig, re_vals, im_vals, label= " -1/N(A): $label " , lw= 2.5 , arrow= true , hover= A_range)
52+
53+ # Mark the start (low A) and end (high A)
54+ scatter! (fig, [re_vals[1 ]], [im_vals[1 ]], label= " Low A" , markershape= :circle )
55+ scatter! (fig, [re_vals[end ]], [im_vals[end ]], label= " High A" , markershape= :square )
56+
57+ return fig
58+ end
59+
60+ # ==============================================================================
61+ # # Examples of usage
62+ # ==============================================================================
63+ # 1. Sign (Relay) function: f(x) = sgn(x)
64+ relay (x) = sign (x)
65+ println (" Relay N(2): " , compute_describing_function (relay, 2.0 ))
66+ # Theoretical check: 4/(π*A) = 4/(2π) ≈ 0.6366
67+
68+ # 2. Saturation function
69+ saturation (x, limit= 1.0 ) = clamp (x, - limit, limit)
70+ println (" Saturation N(2): " , compute_describing_function (x -> saturation (x, 1.0 ), 2.0 ))
71+
72+ # 3. Dead-zone function
73+ deadzone (x, d= 0.1 ) = abs (x) < d ? 0.0 : x - sign (x)* d
74+ println (" Dead-zone N(2): " , compute_describing_function (x -> deadzone (x, 0.1 ), 2.0 ))
75+
76+
77+ # ==============================================================================
78+ # # Nyquist Plot with Describing Function Overlay
79+ # ==============================================================================
80+
81+ using ControlSystems, Plots
82+
83+ nonlin = saturation
84+
85+ s = tf (" s" )
86+ G = 10 / (s^ 3 + 2 s^ 2 + s + 1 )
87+ # Create the linear Nyquist plot first
88+ p_nyq = nyquistplot (G)
89+ # Overlay the describing function
90+ plot_negative_inverse_df (nonlin, label= " deadzone" ; fig= p_nyq,
91+ A_range= 0.01 : 0.01 : 100 )
92+ # The intersection between the Nyquist plot and -1/N(A) indicates potential limit cycles. The amplitude of the limit cycle can be estimated from the corresponding A value, use the plotly() backend to display the amplitude on hover.
93+
94+ # ==============================================================================
95+ # # Simulation
96+ # We can simulate the closed-loop system with the nonlinearity to verify the limit cycle prediction.
97+ # ==============================================================================
98+
99+ nl = nonlinearity (nonlin)
100+ sys_cl = feedback (G* nl)
101+ plot (step (sys_cl, 200 ))
0 commit comments