Skip to content
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

WIP: Adaptive sampling #532

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ControlSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ include("types/zpk.jl")
include("types/lqg.jl") # QUESTION: is it really motivated to have an LQG type?

include("utilities.jl")
include("sampler.jl")
olof3 marked this conversation as resolved.
Show resolved Hide resolved

include("types/promotion.jl")
include("types/conversion.jl")
Expand Down
65 changes: 65 additions & 0 deletions src/freqresp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,66 @@ of system `sys` over the frequency vector `w`."""
[evalfr(sys[i,j], s)[] for s in s_vec, i in 1:ny, j in 1:nu]
end

# TODO Most of this logic should be moved to the respective options, e.g. bode
# TODO Less copying of code
@autovec () function freqresp(sys::LTISystem, lims::Tuple; style=:none)
mfalt marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all the different scalings really necessary? It seems quite natural to only focus on just the complex-valued frequency response. It would definitely simplify the code, and also how one thinks about it conceptually. If done properly, I am not sure that there would be much to gain from using different scalings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure that it makes a difference in reducing the number of points in loglog scale for example.

# Create imaginary freq vector s
func, xscale, yscales = if iscontinuous(sys)
if style === :none
f = (w) -> (evalfr(sys, w*im),)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code duplication is an excellent case for that we should define freqresp for Real (possibly Number) inputs. I don't see any conflict with that we may return a struct when the input is a vector. This would be an incredible QOL improvement in a lot of other situations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created an eval_frequency(sys::LTISystem, w::Real) for now. But I agree that we should do something here.

(f, (identity, identity), (identity,))
elseif style === :bode
f = (w) -> begin
fr = evalfr(sys, w*im)
(fr, abs.(fr), atan.(imag.(fr), real.(fr)))
end
(f, (log10, exp10), (identity, log10, identity))
elseif style === :nyquist
f = (w) -> begin
fr = evalfr(sys, w*im)
(fr, real.(fr), imag.(fr))
end
(f, (identity, identity), (identity,identity,identity))
elseif style === :sigma
f = (w) -> begin
fr = evalfr(sys, w*im)
(fr, svdvals(fr))
end
(f, (log10, exp10), (identity,log10))
else
throw(ArgumentError("Invalid style $style in freqresp"))
end
else
if style === :none
f = (w) -> (evalfr(sys, exp(w*(im*sys.Ts))),)
(f, (identity, identity), (identity,))
elseif style === :bode
f = (w) -> begin
fr = evalfr(sys, exp(w*(im*sys.Ts)))
(fr, abs.(fr), atan.(imag.(fr), real.(fr)))
end
(f, (log10, exp10), (identity, log10, identity))
elseif style === :nyquist
f = (w) -> begin
fr = evalfr(sys, exp(w*(im*sys.Ts)))
(fr, real.(fr), imag.(fr))
end
(f, (identity, identity), (identity,identity,identity))
elseif style === :sigma
f = (w) -> begin
fr = evalfr(sys, exp(w*(im*sys.Ts)))
(fr, svdvals(fr))
end
(f, (log10, exp10), (identity,log10))
else
throw(ArgumentError("Invalid style $style in freqresp"))
end
end

ys, grid = sample(func, lims, xscale, yscales)
return cat(ys[1]..., dims=3), grid
end

# Implements algorithm found in:
# Laub, A.J., "Efficient Multivariable Frequency Response Computations",
# IEEE Transactions on Automatic Control, AC-26 (1981), pp. 407-408.
Expand Down Expand Up @@ -107,6 +167,11 @@ at frequencies `w`
end
@autovec (1, 2) bode(sys::LTISystem) = bode(sys, _default_freq_vector(sys, Val{:bode}()))

@autovec (1, 2) function bode(sys::LTISystem, lims::Tuple)
resp, grid = freqresp(sys, lims, style=:bode)
abs.(resp), rad2deg.(unwrap!(angle.(resp),3)), grid
end

"""`re, im, w = nyquist(sys[, w])`

Compute the real and imaginary parts of the frequency response of system `sys`
Expand Down
120 changes: 120 additions & 0 deletions src/sampler.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@


"""
values, grid = sample(func, lims::Tuple, xscale, yscales; start_gridpoints, maxgridpoints, mineps, reltol_dd, abstol_dd)

Arguments:
Compute a `grid` that tries to capture all features of the function `func` in the range `lim=(xmin,xmax)`.
`func`: `Function` so that that returns a `Tuple` of values, e.g. `func(x) = (v1,v2,...,vN)` where each `vi` is a scalar or AbstractArray.
note that the behavior for non scalar vi might be unintuitive.
The algorithm will try to produce a grid so that features are captured in all of the values, but widely varying scaled might cause problems.
`xscale`: `Tuple` containing a monotone function and its inverse corresponding to the default axis.
Example: If you want to produce a plot with logarithmic x-axis, then you should set `xscale=(log10, exp10)`.
`yscales`: `Tuple` containing one monotone function for each of the values `(v1,v2,...,vN)` corresponding to the default axis.

Kwargs:
`start_gridpoints`: The number of initial grid points.
`maxgridpoints`: A warning will be thrown if this number of gridpoints are reached.
`mineps`: Lower limit on how small steps will be taken (in `xscale`).
Example: with `xscale=(log10, exp10)` and `eps=1e-2`then `log10(grid[k+1])-log10(grid[k]) > (1e-2)/2`, i.e `grid[k+1] > 1.012grid[k]`.
`reltol_dd = 0.05`, `abstol_dd = 0.01`: The criteria for further refinement is
`norm(2*y2-y1-y3) < max(reltol_dd*max(norm(y2-y1), norm(y3-y2)), abstol_dd)`,
where `y1,y2,y3` (in yscale) are 3 equally spaced points (in xscale).

Output:
`values`: `Tuple` with one vector per output value of `func`.
`grid`: `Vector` with the grid corresponding to the values, i.e `func(grid[i])[j] = values[j][i]`

Example: The following code computes a grid and plots the functions: abs(sinc(x)) and cos(x)+1
in lin-log and log-log plots respectively.

func = x -> (abs(sinc(x)),cos(x)+1)
lims = (0.1,7.0)
xscale = (log10, exp10)
yscales = (identity, log10)
y, x = sample(func, lims, xscale, yscales, mineps=1e-3)

plot(x, y[1], m=:o; xscale=:log10, layout=(2,1), yscale=:identity, subplot=1, lab="sinc(x)", size=(800,600))
plot!(x, y[2], m=:o, xscale=:log10, subplot=2, yscale=:log10, lab="cos(x)+1", ylims=(1e-5,10))

"""
function sample(func, lims::Tuple, xscale::Tuple, yscales::Tuple;
start_gridpoints=30, maxgridpoints=2000,
mineps=(xscale[1](lims[2])-xscale[1](lims[1]))/10000,
reltol_dd=0.05, abstol_dd=1e-2)

# Linearly spaced grid in xscale
init_grid = LinRange(xscale[1](lims[1]), xscale[1](lims[2]), start_gridpoints)
# Current count of number mindpoints
num_gridpoints = start_gridpoints

# Current left point
xleft = init_grid[1]
xleft_identity = xscale[2](xleft)
leftvalue = func(xleft_identity)
# The full set of gridpoints
grid = [xleft_identity,]
# Tuple with list of all values (Faster than list of tuples + reshaping)
values = ntuple(i -> [leftvalue[i],], length(yscales))

for xright in init_grid[2:end]
# Scale back to identity
xright_identity = xscale[2](xright)
rightvalue = func(xright_identity)
# Refine (if nessesary) section (xleft,xright)
num_gridpoints = refine_grid!(values, grid, func, xleft, xright, leftvalue, rightvalue, xscale, yscales, maxgridpoints, mineps, num_gridpoints; reltol_dd, abstol_dd)
# We are now done with [xleft,xright]
# Continue to next segment
xleft = xright
leftvalue = rightvalue
end
return values, grid
end

function push_multiple!(vecs::Tuple, vals::Tuple)
push!.(vecs, vals)
end

# Given range (xleft, xright) potentially add more gridpoints. xleft is assumed to be already added, xright will be added
function refine_grid!(values, grid, func, xleft, xright, leftvalue, rightvalue, xscale, yscales, maxgridpoints, mineps, num_gridpoints; reltol_dd=0.05, abstol_dd=1e-2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that , x1, x2, xm/xmid, y2, y2, etc., alternatively xa, xb, ya, yb would make this a lot easier to parse.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You partly already do this in is_almost_linear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right

# In scaled grid
midpoint = (xleft+xright)/2
# Scaled back
midpoint_identity = xscale[2](midpoint)
midvalue = func(midpoint_identity)

#mineps in scaled version, abs to avoid assuming monotonly increasing scale
if (abs(xright - xleft) >= mineps) && (num_gridpoints < maxgridpoints) && !is_almost_linear(xleft, midpoint, xright, leftvalue, midvalue, rightvalue, xscale, yscales; reltol_dd, abstol_dd)
num_gridpoints = refine_grid!(values, grid, func, xleft, midpoint, leftvalue, midvalue, xscale, yscales, maxgridpoints, mineps, num_gridpoints; reltol_dd, abstol_dd)
num_gridpoints = refine_grid!(values, grid, func, midpoint, xright, midvalue, rightvalue, xscale, yscales, maxgridpoints, mineps, num_gridpoints; reltol_dd, abstol_dd)
else
# No more refinement needed, add computed points
push!(grid, midpoint_identity)
push!(grid, xscale[2](xright))
push_multiple!(values, midvalue)
push_multiple!(values, rightvalue)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why create a specific method for this and not just use the one line implementation in the method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It used to be a weird implementation with 4 different method. Could definetely be inlined now

num_gridpoints += 2
end
return num_gridpoints
end

# TODO We can scale when saving instead of recomputing scaling
# Svectors should also be faster in general
function is_almost_linear(xleft, midpoint, xright, leftvalue, midvalue, rightvalue, xscale, yscales; reltol_dd=0.05, abstol_dd=1e-2)
# We assume that x2-x1 \approx x3-x1, so we need to check that y2-y1 approx y3-y2
mfalt marked this conversation as resolved.
Show resolved Hide resolved

# x1 = xscale.(xleft)
# x2 = xscale.(midpoint)
# x3 = xscale.(xright)
y1 = apply_tuple2(yscales, leftvalue)
y2 = apply_tuple2(yscales, midvalue)
y3 = apply_tuple2(yscales, rightvalue)
d1 = (y2-y1)
d2 = (y3-y2)
# Essentially low second derivative compared to derivative, so that linear approximation is good.
# Second argument to avoid too small steps when derivatives are small
norm(d1-d2) < max(reltol_dd*max(norm(d1), norm(d2)), abstol_dd)
end

# Seems to be fast enough
apply_tuple2(fs::Tuple, x) = [fs[i].(x[i]) for i in 1:length(fs)]