Skip to content

Commit c10fe6b

Browse files
committed
move root locus implementation to ControlSystemsBase
1 parent 3b1a46d commit c10fe6b

File tree

9 files changed

+96
-34
lines changed

9 files changed

+96
-34
lines changed

docs/src/lib/plotting.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Pages = [libpath*"/plotting.jl"]
1616
Order = [:function]
1717
Private = false
1818
```
19+
```@docs
20+
ControlSystemsBase.rlocusplot
21+
```
22+
- To plot simulation results such as step and impulse responses, use `plot(::SimResult)`, see also [`lsim`](@ref).
1923

2024
## Examples
2125

docs/src/man/introduction.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ For workflows that do not require continuous-time simulation, you may instead op
1010
```julia
1111
using Pkg; Pkg.add("ControlSystemsBase")
1212
```
13-
ControlSystemsBase contains all functionality of ControlSystems except continuous-time simulation and root locus, and is *considerably* faster to load and precompile. To enjoy the faster pre-compilation, do not even install ControlSystems since this will cause pre-compilation of OrdinaryDiffEq, which can take several minutes.
13+
ControlSystemsBase contains all functionality of ControlSystems except continuous-time simulation, and is *considerably* faster to load and precompile. To enjoy the faster pre-compilation, do not even install ControlSystems since this will cause pre-compilation of OrdinaryDiffEq, which can take several minutes.
1414

1515
## Basic functions
1616
```@meta
1717
DocTestSetup = quote
1818
using ControlSystems
1919
P = tf([1],[1,1])
20-
T = P/(1+P)
20+
T = feedback(P)
2121
plotsDir = joinpath(dirname(pathof(ControlSystems)), "..", "docs", "build", "plots")
2222
mkpath(plotsDir)
2323
save_docs_plot(name) = Plots.savefig(joinpath(plotsDir,name))
@@ -29,7 +29,7 @@ State-space systems can be created using the function [`ss`](@ref) and transfer
2929
Example:
3030
```jldoctest INTRO
3131
P = tf([1.0],[1,1])
32-
T = P/(1+P)
32+
T = P/(1+P) # Not recommended, see below
3333
3434
# output
3535
@@ -74,3 +74,8 @@ using Plots
7474
bodeplot(tf(1,[1,2,1]))
7575
```
7676

77+
78+
Step, impulse and other simulation responses do not have their own plot functions, instead, call `plot` on the result of the simulation function:
79+
```@example INTRO
80+
plot(step(tf(1,[1,0.5,1]), 20))
81+
```

lib/ControlSystemsBase/src/ControlSystemsBase.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export LTISystem,
112112
pade,
113113
thiran,
114114
nonlinearity,
115+
rlocus,
116+
rlocusplot,
115117
# demo systems
116118
ssrand,
117119
DemoSystems, # A module containing some example systems

src/root_locus.jl renamed to lib/ControlSystemsBase/src/root_locus.jl

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,101 @@ import ControlSystemsBase.RootLocusResult
33
@userplot Rlocusplot
44

55

6-
function getpoles(G, K::Number; kwargs...)
6+
function getpoles(G, K::Number; tol = 1e-2, initial_stepsize = 1e-3, kwargs...)
77
issiso(G) || error("root locus only supports SISO systems")
88
G isa TransferFunction || (G = tf(G))
99
P = numpoly(G)[]
1010
Q = denpoly(G)[]
1111
T = float(typeof(K))
1212
ϵ = eps(T)
1313
nx = length(Q)
14-
D = zeros(nx-1, nx-1) # distance matrix
15-
prevpoles = ComplexF64[]
14+
15+
# Scale tolerance with system order
16+
tol = tol * (nx - 1)
17+
18+
poleout_list = Vector{Vector{ComplexF64}}() # To store pole sets at each accepted step
19+
k_scalars_collected = Float64[] # To store accepted k_scalar values
20+
21+
prevpoles = ComplexF64[] # Initialize prevpoles for the first iteration
1622
temppoles = zeros(ComplexF64, nx-1)
17-
f = function (_,_,k)
23+
D = zeros(nx-1, nx-1) # distance matrix
24+
25+
stepsize = initial_stepsize
26+
k_scalar = 0.0
27+
28+
# Function to compute poles for a given k value
29+
compute_poles = function(k)
1830
if k == 0 && length(P) > length(Q)
1931
# More zeros than poles, make sure the vector of roots is of correct length when k = 0
2032
# When this happens, there are fewer poles for k = 0, these poles can be seen as being located somewhere at Inf
2133
# We get around the problem by not allowing k = 0 for non-proper systems.
2234
k = ϵ
2335
end
24-
newpoles = ComplexF64.(Polynomials.roots(k[1]*P+Q))
36+
ComplexF64.(Polynomials.roots(k*P+Q))
37+
end
38+
39+
# Initial poles at k_scalar = 0.0
40+
initial_poles = compute_poles(0.0)
41+
push!(poleout_list, initial_poles)
42+
push!(k_scalars_collected, 0.0)
43+
prevpoles = initial_poles # Set prevpoles for the first actual step
44+
45+
while k_scalar < K
46+
# Propose a new k_scalar value
47+
next_k_scalar = min(K, k_scalar + stepsize)
48+
49+
# Calculate poles for the proposed next_k_scalar
50+
current_poles_proposed = compute_poles(next_k_scalar)
51+
52+
# Calculate cost using Hungarian algorithm
2553
if !isempty(prevpoles)
26-
D .= abs.(newpoles .- transpose(prevpoles))
54+
D .= abs.(current_poles_proposed .- transpose(prevpoles))
2755
assignment, cost = Hungarian.hungarian(D)
28-
for i = 1:nx-1
29-
temppoles[assignment[i]] = newpoles[i]
56+
else
57+
cost = 0.0
58+
assignment = collect(1:nx-1)
59+
end
60+
61+
# Adaptive step size logic
62+
if cost > 2 * tol # Cost is too high, reject step and reduce stepsize
63+
stepsize /= 2.0
64+
# Ensure stepsize doesn't become too small
65+
if stepsize < 100*eps(T)
66+
@warn "Step size became extremely small, potentially stuck. Breaking loop."
67+
break
68+
end
69+
# Do not update k_scalar, try again with smaller stepsize
70+
else # Step is acceptable
71+
# Sort poles using the assignment from Hungarian algorithm
72+
if !isempty(prevpoles)
73+
for i = 1:nx-1
74+
temppoles[assignment[i]] = current_poles_proposed[i]
75+
end
76+
current_poles_sorted = copy(temppoles)
77+
else
78+
current_poles_sorted = current_poles_proposed
79+
end
80+
81+
# Accept the step
82+
push!(poleout_list, current_poles_sorted)
83+
push!(k_scalars_collected, next_k_scalar)
84+
prevpoles = current_poles_sorted # Update prevpoles for the next iteration
85+
k_scalar = next_k_scalar # Advance k_scalar
86+
87+
if cost < tol # Cost is too low, increase stepsize
88+
stepsize *= 1.1
3089
end
31-
newpoles .= temppoles
90+
# Cap stepsize to prevent overshooting K significantly in a single step
91+
stepsize = min(stepsize, K/10)
92+
end
93+
94+
# Break if k_scalar has reached or exceeded K
95+
if k_scalar >= K
96+
break
3297
end
33-
prevpoles = newpoles
34-
newpoles
35-
end
36-
prob = OrdinaryDiffEq.ODEProblem(f,f(0.,0.,0),(0,K[end]))
37-
integrator = OrdinaryDiffEq.init(prob,OrdinaryDiffEq.Tsit5(),reltol=1e-8,abstol=1e-8)
38-
ts = Vector{Float64}()
39-
poleout = Vector{Vector{ComplexF64}}()
40-
push!(poleout,integrator.k[1])
41-
push!(ts,0)
42-
for i in integrator
43-
push!(poleout,integrator.k[end])
44-
push!(ts,integrator.t[1])
4598
end
46-
poleout = copy(hcat(poleout...)')
47-
poleout, ts
99+
100+
return hcat(poleout_list...)' |> copy, k_scalars_collected
48101
end
49102

50103

@@ -169,7 +222,7 @@ end
169222
"""
170223
roots, Z, K = rlocus(P::LTISystem, K = 500)
171224
172-
Compute the root locus of the SISO LTISystem `P` with a negative feedback loop and feedback gains between 0 and `K`. `rlocus` will use an adaptive step-size algorithm to determine the values of the feedback gains used to generate the plot.
225+
Compute the root locus of the LTISystem `P` with a negative feedback loop and feedback gains between 0 and `K`. `rlocus` will use an adaptive step-size algorithm to determine the values of the feedback gains used to generate the plot.
173226
174227
`roots` is a complex matrix containing the poles trajectories of the closed-loop `1+k⋅G(s)` as a function of `k`, `Z` contains the zeros of the open-loop system `G(s)` and `K` the values of the feedback gain.
175228
@@ -236,8 +289,9 @@ end
236289

237290
"""
238291
rlocusplot(P::LTISystem; K)
292+
rlocusplot(P::StateSpace, K::Matrix; output = false)
239293
240-
Plot the root locus of the SISO LTISystem `P` as computed by `rlocus`.
294+
Plot the root locus of the LTISystem `P` as computed by `rlocus`.
241295
"""
242296
@recipe function rlocusplot(::Type{Rlocusplot}, p::Rlocusplot; K=500, output=false)
243297
if length(p.args) >= 2

lib/ControlSystemsBase/test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ my_tests = [
4343
"test_plots",
4444
"test_dsp",
4545
"test_implicit_diff",
46+
"test_rootlocus.jl",
47+
"test_root_locus_matrix.jl",
4648
]
4749

4850
@testset "All Tests" begin
File renamed without changes.

src/ControlSystems.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ using StaticArrays
1616
using RecipesBase
1717
using Printf
1818

19-
export Simulator, rlocus, rlocusplot
19+
export Simulator
2020

2121
include("timeresp.jl")
2222
include("simulators.jl")

test/runtests_controlsystems.jl

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,4 @@ using ControlSystems
1717
include("test_nonlinear_timeresp.jl")
1818
end
1919

20-
@testset "rootlocus" begin
21-
@info "Testing rootlocus"
22-
include("test_rootlocus.jl")
23-
include("test_root_locus_matrix.jl")
24-
end
2520
end

0 commit comments

Comments
 (0)