Skip to content

Commit 15ffcd8

Browse files
authored
Merge pull request #1041 from JuliaControl/hysteresis
Add hysteresis nonlinearity
2 parents 353ed12 + 4955641 commit 15ffcd8

6 files changed

Lines changed: 110 additions & 14 deletions

File tree

docs/Project.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ ImplicitDifferentiation = "57b37032-215b-411a-8a7c-41a003a55207"
1212
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
1313
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1414
MonteCarloMeasurements = "0987c9cc-fe09-11e8-30f0-b96dd679fdca"
15-
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
1615
OptimizationGCMAES = "6f0a0517-dbc2-4a7a-8a20-99ae7f27e911"
17-
OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1"
16+
OptimizationIpopt = "43fad042-7963-4b32-ab19-e2a4f9a67124"
1817
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
1918
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2019
TrajectoryLimiters = "9792e600-fe43-4e4e-833b-462f466b8006"

docs/src/examples/automatic_differentiation.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ We start by defining a helper function `plot_optimized` that will evaluate the p
108108
The constraint function `constraints` enforces the peak of the sensitivity function to be below `Msc`. Finally, we use [Optimization.jl](https://github.com/SciML/Optimization.jl) to optimize the cost function and tell it to use ForwardDiff.jl to compute the gradient of the cost function. The optimizer we use in this example is `Ipopt`.
109109

110110
```@example autodiff
111-
using Optimization, Statistics, LinearAlgebra
112-
using Ipopt, OptimizationMOI; MOI = OptimizationMOI.MOI
111+
using Statistics, LinearAlgebra
112+
using OptimizationIpopt
113113
114114
function plot_optimized(P, params, res, systems)
115115
fig = plot(layout=(1,3), size=(1200,400), bottommargin=2Plots.mm)
@@ -170,15 +170,18 @@ Msc = 1.3 # Constraint on Ms
170170
171171
params = [kp, ki, kd, 0.01] # Initial guess for parameters
172172
173-
solver = Ipopt.Optimizer()
174-
MOI.set(solver, MOI.RawOptimizerAttribute("print_level"), 0)
175-
MOI.set(solver, MOI.RawOptimizerAttribute("max_iter"), 200)
176-
MOI.set(solver, MOI.RawOptimizerAttribute("acceptable_tol"), 1e-1)
177-
MOI.set(solver, MOI.RawOptimizerAttribute("acceptable_constr_viol_tol"), 1e-2)
178-
MOI.set(solver, MOI.RawOptimizerAttribute("acceptable_iter"), 5)
179-
MOI.set(solver, MOI.RawOptimizerAttribute("hessian_approximation"), "limited-memory")
173+
solver = IpoptOptimizer(;
174+
acceptable_tol = 1e-1,
175+
acceptable_iter = 5,
176+
hessian_approximation = "limited-memory",
177+
additional_options = Dict(
178+
"print_level" => 0,
179+
"max_iter" => 200,
180+
"acceptable_constr_viol_tol" => 1e-2,
181+
),
182+
)
180183
181-
fopt = OptimizationFunction(cost, Optimization.AutoForwardDiff(); cons=constraints)
184+
fopt = OptimizationFunction(cost, OptimizationIpopt.AutoForwardDiff(); cons=constraints)
182185
183186
prob = OptimizationProblem(fopt, params, (P, systemspid);
184187
lb = fill(-10.0, length(params)),

docs/src/lib/nonlinear.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,39 @@ f2 = plot(step(feedback(duffing, C), 8), plotx=true, plot_title="Controlled wigg
229229
plot(f1,f2, size=(1300,800))
230230
```
231231

232+
### Hysteresis
233+
Here we demonstrate that we may use this simple framework to model also stateful nonlinearities, such as hysteresis. The `hysteresis` function internally creates a feedback interconnection between a fast first-order system and a `sign` or `tanh` nonlinearity to create a simple hysteresis loop. The width and amplitude of the loop can be adjusted through the parameters `width` and `amplitude`, respectively.
234+
```@example HYSTERESIS
235+
using ControlSystems, Plots
236+
import ControlSystemsBase: hysteresis
237+
238+
amplitude = 0.7
239+
width = 1.5
240+
sys_hyst = hysteresis(; width, amplitude)
241+
242+
t = 0:0.01:20
243+
ufun(y,x,t) = y .= 5.0 .* sin(t) ./ (1+t/5) # A sine wave that sweeps back and forth with decreasing amplitude
244+
245+
res = lsim(sys_hyst, ufun, t)
246+
247+
p1 = plot(res.u[:], res.y[:],
248+
title="Hysteresis Loop",
249+
xlabel="Input u(t)",
250+
ylabel="Output y(t)",
251+
lw=2, color=:blue, lab="", framestyle=:zerolines)
252+
253+
hline!([amplitude -amplitude], l=:dash, c=:red, label=["Amplitude" ""])
254+
vline!([width -width], l=:dash, c=:green, label=["Width" ""])
255+
256+
# Plotting time series to see the "jumps" in the response
257+
p2 = plot(t, [res.u[:] res.y[:]],
258+
title="Time Domain Response",
259+
label=["Input (Sine)" "Output (Hysteresis)"],
260+
xlabel="Time (s)",
261+
lw=2)
262+
263+
plot(p1, p2, layout=(1,2), size=(900, 400))
264+
```
232265

233266
## Limitations
234267
- Remember, this functionality is experimental and subject to breakage.
@@ -260,4 +293,5 @@ ControlSystemsBase.saturation
260293
ControlSystemsBase.ratelimit
261294
ControlSystemsBase.deadzone
262295
ControlSystemsBase.linearize
296+
ControlSystemsBase.hysteresis
263297
```

lib/ControlSystemsBase/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ ComponentArrays = "0.15"
3636
DSP = "0.6.1, 0.7, 0.8"
3737
ForwardDiff = "0.10, 1.0"
3838
Hungarian = "0.7.0"
39-
ImplicitDifferentiation = "0.9"
39+
ImplicitDifferentiation = "0.8, 0.9"
4040
LinearAlgebra = "<0.0.1, 1"
4141
MacroTools = "0.5"
4242
Makie = "0.22, 0.23, 0.24"

lib/ControlSystemsBase/src/nonlinear_components.jl

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,42 @@ See [`ControlSystemsBase.offset`](@ref) for handling operating points.
131131
"""
132132
deadzone(args...) = nonlinearity(DeadZone(args...))
133133
deadzone(v::AbstractVector, args...) = nonlinearity(DeadZone.(v, args...))
134-
Base.show(io::IO, f::DeadZone) = f.u == -f.l ? print(io, "deadzone($(f.u))") : print(io, "deadzone($(f.l), $(f.u))")
134+
Base.show(io::IO, f::DeadZone) = f.u == -f.l ? print(io, "deadzone($(f.u))") : print(io, "deadzone($(f.l), $(f.u))")
135+
136+
137+
## Hysteresis ==================================================================
138+
139+
"""
140+
hysteresis(; amplitude, width, Tf, hardness)
141+
142+
Create a hysteresis nonlinearity. The signal switches between `±amplitude` when the input crosses `±width`. `Tf` controls the time constant of the internal state that tracks the hysteresis, and `hardness` controls how sharp the transition is between the two states.
143+
144+
```
145+
146+
y▲
147+
148+
amp┌───┼───┬─►
149+
│ │ ▲
150+
│ │ │
151+
──┼───┼───┼───► u
152+
│ │ │w
153+
▼ │ │
154+
◄─┴───┼───┘
155+
156+
```
157+
158+
$nonlinear_warning
159+
"""
160+
function hysteresis(; amplitude=1.0, width=1.0, Tf=0.001, hardness=20.0)
161+
T = promote_type(typeof(amplitude), typeof(width), typeof(Tf), typeof(hardness))
162+
G = tf(1, [Tf, 1])
163+
if isfinite(hardness)
164+
nl_func = y -> width * tanh(hardness*y)
165+
else
166+
nl_func = y -> width * sign(y)
167+
end
168+
nl = nonlinearity(nl_func)
169+
amplitude/width*(feedback(G, -nl) - 1)
170+
end
171+
172+

test/test_nonlinear_timeresp.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,25 @@ res = lsim(nl, (x,t)->2sin(t), 0:0.01:4pi)
235235
@test minimum(res.y) -0.6 rtol=1e-3
236236
@test maximum(res.y) 1.3 rtol=1e-3
237237

238+
## Hysteresis
239+
using ControlSystemsBase: hysteresis
240+
241+
# Construction
242+
@test hysteresis() isa HammersteinWienerSystem
243+
@test hysteresis(; amplitude=0.7, width=1.5) isa HammersteinWienerSystem
244+
245+
# Simulate with sine input and verify output bounded by amplitude
246+
amplitude = 0.7
247+
width = 1.5
248+
sys_hyst = hysteresis(; width, amplitude)
249+
t = 0:0.001:10
250+
res = lsim(sys_hyst, (x,t) -> 5sin(t), t)
251+
@test maximum(res.y) amplitude rtol=0.1
252+
@test minimum(res.y) -amplitude rtol=0.1
253+
254+
# Finite hardness (smooth tanh transition)
255+
sys_hyst_smooth = hysteresis(; width, amplitude, hardness=5.0)
256+
res_smooth = lsim(sys_hyst_smooth, (x,t) -> 5sin(t), t)
257+
@test maximum(res_smooth.y) amplitude rtol=0.15
258+
@test minimum(res_smooth.y) -amplitude rtol=0.15
259+

0 commit comments

Comments
 (0)