Skip to content

Commit 801c888

Browse files
authored
move root locus implementation to ControlSystemsBase (#1013)
* move root locus implementation to ControlSystemsBase * better error messages * rm import * include file * add docs * update paths * update deps * rm import * import Hungarian
1 parent 3b1a46d commit 801c888

File tree

11 files changed

+113
-40
lines changed

11 files changed

+113
-40
lines changed

Project.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
99
DelayDiffEq = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb"
1010
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
1111
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
12-
Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39"
1312
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1413
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
1514
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
@@ -25,7 +24,6 @@ ControlSystemsBase = "1.3"
2524
DelayDiffEq = "5.31"
2625
DiffEqCallbacks = "2.16, 3, 4"
2726
ForwardDiff = "0.10, 1"
28-
Hungarian = "0.6, 0.7"
2927
OrdinaryDiffEq = "6.60"
3028
RecipesBase = "1"
3129
Reexport = "1"

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/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ version = "1.17.5"
66

77
[deps]
88
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
9+
Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39"
910
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1011
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1112
MatrixEquations = "99c1a7ee-ab34-5fd5-8076-27c950a045f4"
@@ -32,6 +33,7 @@ Aqua = "0.5"
3233
ComponentArrays = "0.15"
3334
DSP = "0.6.1, 0.7, 0.8"
3435
ForwardDiff = "0.10, 1.0"
36+
Hungarian = "0.7.0"
3537
ImplicitDifferentiation = "0.7.2"
3638
LinearAlgebra = "<0.0.1, 1"
3739
MacroTools = "0.5"

lib/ControlSystemsBase/src/ControlSystemsBase.jl

Lines changed: 5 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
@@ -136,6 +138,7 @@ import Base: +, -, *, /, (==), (!=), isapprox, convert, promote_op
136138
import Base: getproperty, getindex
137139
import Base: exp # for exp(-s)
138140
import LinearAlgebra: BlasFloat
141+
import Hungarian
139142

140143
export lyap # Make sure LinearAlgebra.lyap is available
141144
export plyap
@@ -211,6 +214,8 @@ include("types/staticsystems.jl")
211214

212215
include("plotting.jl")
213216

217+
include("root_locus.jl")
218+
214219
@deprecate pole poles
215220
@deprecate tzero tzeros
216221
@deprecate num numvec

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

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,117 @@ const Polynomials = ControlSystemsBase.Polynomials
22
import ControlSystemsBase.RootLocusResult
33
@userplot Rlocusplot
44

5+
"""
6+
rlocusplot(siso_sys)
7+
rlocusplot(sys::StateSpace, K::Matrix; output=false)
8+
9+
Plot the root locus of a system under feedback.
10+
11+
If a SISO system is passed, the feedback gain `K` is a scalar that ranges from 0 to `K` (if provided). If a `StateSpace` system is passed, `K` is a matrix that defines the feedback gain, and the poles are computed as `K` ranges from `0*K` to `1*K`. In this case, `K` is assumed to be a state-feedback matrix of dimension `(nu, nx)`. To compute the poles for output feedback, pass `output = true` and `K` of dimension `(nu, ny)`.
12+
"""
13+
rlocusplot
14+
515

6-
function getpoles(G, K::Number; kwargs...)
7-
issiso(G) || error("root locus only supports SISO systems")
16+
function getpoles(G, K::Number; tol = 1e-2, initial_stepsize = 1e-3, kwargs...)
17+
issiso(G) || error("root locus with scalar gain only supports SISO systems, did you intend to pass a feedback gain matrix `K`?")
818
G isa TransferFunction || (G = tf(G))
919
P = numpoly(G)[]
1020
Q = denpoly(G)[]
1121
T = float(typeof(K))
1222
ϵ = eps(T)
1323
nx = length(Q)
14-
D = zeros(nx-1, nx-1) # distance matrix
15-
prevpoles = ComplexF64[]
24+
25+
# Scale tolerance with system order
26+
tol = tol * (nx - 1)
27+
28+
poleout_list = Vector{Vector{ComplexF64}}() # To store pole sets at each accepted step
29+
k_scalars_collected = Float64[] # To store accepted k_scalar values
30+
31+
prevpoles = ComplexF64[] # Initialize prevpoles for the first iteration
1632
temppoles = zeros(ComplexF64, nx-1)
17-
f = function (_,_,k)
33+
D = zeros(nx-1, nx-1) # distance matrix
34+
35+
stepsize = initial_stepsize
36+
k_scalar = 0.0
37+
38+
# Function to compute poles for a given k value
39+
compute_poles = function(k)
1840
if k == 0 && length(P) > length(Q)
1941
# More zeros than poles, make sure the vector of roots is of correct length when k = 0
2042
# When this happens, there are fewer poles for k = 0, these poles can be seen as being located somewhere at Inf
2143
# We get around the problem by not allowing k = 0 for non-proper systems.
2244
k = ϵ
2345
end
24-
newpoles = ComplexF64.(Polynomials.roots(k[1]*P+Q))
46+
ComplexF64.(Polynomials.roots(k*P+Q))
47+
end
48+
49+
# Initial poles at k_scalar = 0.0
50+
initial_poles = compute_poles(0.0)
51+
push!(poleout_list, initial_poles)
52+
push!(k_scalars_collected, 0.0)
53+
prevpoles = initial_poles # Set prevpoles for the first actual step
54+
55+
while k_scalar < K
56+
# Propose a new k_scalar value
57+
next_k_scalar = min(K, k_scalar + stepsize)
58+
59+
# Calculate poles for the proposed next_k_scalar
60+
current_poles_proposed = compute_poles(next_k_scalar)
61+
62+
# Calculate cost using Hungarian algorithm
2563
if !isempty(prevpoles)
26-
D .= abs.(newpoles .- transpose(prevpoles))
64+
D .= abs.(current_poles_proposed .- transpose(prevpoles))
2765
assignment, cost = Hungarian.hungarian(D)
28-
for i = 1:nx-1
29-
temppoles[assignment[i]] = newpoles[i]
66+
else
67+
cost = 0.0
68+
assignment = collect(1:nx-1)
69+
end
70+
71+
# Adaptive step size logic
72+
if cost > 2 * tol # Cost is too high, reject step and reduce stepsize
73+
stepsize /= 2.0
74+
# Ensure stepsize doesn't become too small
75+
if stepsize < 100*eps(T)
76+
@warn "Step size became extremely small, potentially stuck. Breaking loop."
77+
break
78+
end
79+
# Do not update k_scalar, try again with smaller stepsize
80+
else # Step is acceptable
81+
# Sort poles using the assignment from Hungarian algorithm
82+
if !isempty(prevpoles)
83+
for i = 1:nx-1
84+
temppoles[assignment[i]] = current_poles_proposed[i]
85+
end
86+
current_poles_sorted = copy(temppoles)
87+
else
88+
current_poles_sorted = current_poles_proposed
89+
end
90+
91+
# Accept the step
92+
push!(poleout_list, current_poles_sorted)
93+
push!(k_scalars_collected, next_k_scalar)
94+
prevpoles = current_poles_sorted # Update prevpoles for the next iteration
95+
k_scalar = next_k_scalar # Advance k_scalar
96+
97+
if cost < tol # Cost is too low, increase stepsize
98+
stepsize *= 1.1
3099
end
31-
newpoles .= temppoles
100+
# Cap stepsize to prevent overshooting K significantly in a single step
101+
stepsize = min(stepsize, K/10)
102+
end
103+
104+
# Break if k_scalar has reached or exceeded K
105+
if k_scalar >= K
106+
break
32107
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])
45108
end
46-
poleout = copy(hcat(poleout...)')
47-
poleout, ts
109+
110+
return hcat(poleout_list...)' |> copy, k_scalars_collected
48111
end
49112

50113

51114
function getpoles(G, K::AbstractVector{T}) where {T<:Number}
52-
issiso(G) || error("root locus only supports SISO systems")
115+
issiso(G) || error("root locus with scalar gain only supports SISO systems, did you intend to pass a feedback gain matrix `K`?")
53116
G isa TransferFunction || (G = tf(G))
54117
P, Q = numpoly(G)[], denpoly(G)[]
55118
poleout = Matrix{ComplexF64}(undef, Polynomials.degree(Q), length(K))
@@ -169,7 +232,7 @@ end
169232
"""
170233
roots, Z, K = rlocus(P::LTISystem, K = 500)
171234
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.
235+
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.
173236
174237
`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.
175238
@@ -236,8 +299,9 @@ end
236299

237300
"""
238301
rlocusplot(P::LTISystem; K)
302+
rlocusplot(P::StateSpace, K::Matrix; output = false)
239303
240-
Plot the root locus of the SISO LTISystem `P` as computed by `rlocus`.
304+
Plot the root locus of the LTISystem `P` as computed by `rlocus`.
241305
"""
242306
@recipe function rlocusplot(::Type{Rlocusplot}, p::Rlocusplot; K=500, output=false)
243307
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",
47+
"test_root_locus_matrix",
4648
]
4749

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

src/ControlSystems.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,17 @@ using ControlSystemsBase: issiso, ninputs, noutputs, nstates, numeric_type
88
using LinearAlgebra
99
import OrdinaryDiffEq
1010
import LinearAlgebra: BlasFloat
11-
import Hungarian # For pole assignment in rlocusplot
1211
import DiffEqCallbacks: SavingCallback, SavedValues
1312
import DelayDiffEq
1413
using SparseArrays
1514
using StaticArrays
1615
using RecipesBase
1716
using Printf
1817

19-
export Simulator, rlocus, rlocusplot
18+
export Simulator
2019

2120
include("timeresp.jl")
2221
include("simulators.jl")
23-
include("root_locus.jl")
2422

2523
# The path has to be evaluated upon initial import
2624
const __CONTROLSYSTEMS_SOURCE_DIR__ = dirname(Base.source_path())

0 commit comments

Comments
 (0)