-
Notifications
You must be signed in to change notification settings - Fork 89
move root locus implementation to ControlSystemsBase #1013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c10fe6b
cd76d92
ee1b373
fbc5c5b
d2c7903
763bbac
f649646
54e7780
80e10cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,54 +2,117 @@ const Polynomials = ControlSystemsBase.Polynomials | |||||
| import ControlSystemsBase.RootLocusResult | ||||||
| @userplot Rlocusplot | ||||||
|
|
||||||
| """ | ||||||
| rlocusplot(siso_sys) | ||||||
| rlocusplot(sys::StateSpace, K::Matrix; output=false) | ||||||
|
|
||||||
| Plot the root locus of a system under feedback. | ||||||
|
|
||||||
| 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)`. | ||||||
| """ | ||||||
| rlocusplot | ||||||
|
|
||||||
|
|
||||||
| function getpoles(G, K::Number; kwargs...) | ||||||
| issiso(G) || error("root locus only supports SISO systems") | ||||||
| function getpoles(G, K::Number; tol = 1e-2, initial_stepsize = 1e-3, kwargs...) | ||||||
| issiso(G) || error("root locus with scalar gain only supports SISO systems, did you intend to pass a feedback gain matrix `K`?") | ||||||
| G isa TransferFunction || (G = tf(G)) | ||||||
| P = numpoly(G)[] | ||||||
| Q = denpoly(G)[] | ||||||
| T = float(typeof(K)) | ||||||
| ϵ = eps(T) | ||||||
| nx = length(Q) | ||||||
| D = zeros(nx-1, nx-1) # distance matrix | ||||||
| prevpoles = ComplexF64[] | ||||||
|
|
||||||
| # Scale tolerance with system order | ||||||
| tol = tol * (nx - 1) | ||||||
|
|
||||||
| poleout_list = Vector{Vector{ComplexF64}}() # To store pole sets at each accepted step | ||||||
| k_scalars_collected = Float64[] # To store accepted k_scalar values | ||||||
|
|
||||||
| prevpoles = ComplexF64[] # Initialize prevpoles for the first iteration | ||||||
| temppoles = zeros(ComplexF64, nx-1) | ||||||
| f = function (_,_,k) | ||||||
| D = zeros(nx-1, nx-1) # distance matrix | ||||||
|
|
||||||
| stepsize = initial_stepsize | ||||||
| k_scalar = 0.0 | ||||||
|
|
||||||
| # Function to compute poles for a given k value | ||||||
| compute_poles = function(k) | ||||||
| if k == 0 && length(P) > length(Q) | ||||||
| # More zeros than poles, make sure the vector of roots is of correct length when k = 0 | ||||||
| # When this happens, there are fewer poles for k = 0, these poles can be seen as being located somewhere at Inf | ||||||
| # We get around the problem by not allowing k = 0 for non-proper systems. | ||||||
| k = ϵ | ||||||
| end | ||||||
| newpoles = ComplexF64.(Polynomials.roots(k[1]*P+Q)) | ||||||
| ComplexF64.(Polynomials.roots(k*P+Q)) | ||||||
| end | ||||||
|
|
||||||
| # Initial poles at k_scalar = 0.0 | ||||||
| initial_poles = compute_poles(0.0) | ||||||
| push!(poleout_list, initial_poles) | ||||||
| push!(k_scalars_collected, 0.0) | ||||||
| prevpoles = initial_poles # Set prevpoles for the first actual step | ||||||
|
|
||||||
| while k_scalar < K | ||||||
| # Propose a new k_scalar value | ||||||
| next_k_scalar = min(K, k_scalar + stepsize) | ||||||
|
|
||||||
| # Calculate poles for the proposed next_k_scalar | ||||||
| current_poles_proposed = compute_poles(next_k_scalar) | ||||||
|
|
||||||
| # Calculate cost using Hungarian algorithm | ||||||
| if !isempty(prevpoles) | ||||||
| D .= abs.(newpoles .- transpose(prevpoles)) | ||||||
| D .= abs.(current_poles_proposed .- transpose(prevpoles)) | ||||||
| assignment, cost = Hungarian.hungarian(D) | ||||||
| for i = 1:nx-1 | ||||||
| temppoles[assignment[i]] = newpoles[i] | ||||||
| else | ||||||
| cost = 0.0 | ||||||
| assignment = collect(1:nx-1) | ||||||
| end | ||||||
|
|
||||||
| # Adaptive step size logic | ||||||
| if cost > 2 * tol # Cost is too high, reject step and reduce stepsize | ||||||
| stepsize /= 2.0 | ||||||
| # Ensure stepsize doesn't become too small | ||||||
| if stepsize < 100*eps(T) | ||||||
|
||||||
| if stepsize < 100*eps(T) | |
| if stepsize < MIN_STEP_SIZE_THRESHOLD |
Copilot
AI
Jul 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The warning message could be more helpful by including the current step size value and suggesting potential causes or solutions.
| @warn "Step size became extremely small, potentially stuck. Breaking loop." | |
| @warn "Step size became extremely small (stepsize = $stepsize). This may indicate numerical instability or poor system conditioning. Consider increasing `tol` or `initial_stepsize`. Breaking loop." |
Copilot
AI
Jul 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number 1.1 should be defined as a named constant to make the step size growth factor more explicit and configurable.
| stepsize *= 1.1 | |
| stepsize *= STEP_SIZE_GROWTH_FACTOR |
Copilot
AI
Jul 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number K/10 should be defined as a named constant to make the maximum step size ratio more explicit and configurable.
| stepsize = min(stepsize, K/10) | |
| stepsize = min(stepsize, MAX_STEP_RATIO * K) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number
2 * tolshould be defined as a named constant to make the cost threshold multiplier more explicit and configurable.