ControlSystems.jl
ControlSystems.jl copied to clipboard
RFC: Implement a "Problem" style synthesis interface
I was thinking about how the synthesis functions are setup currently, and I think currently they are still very "Matlab-y," which I at least find somewhat awkward to work with because you have to pass around every matrix as an argument (this can grow to a very large number of arguments if you start doing things like linear predictive control, where I have written functions with 7 or more different arguments). On the other hand, I think the LQG type is somewhat too much, where it even does the controller design for you in the constructor instead of in a separate function like you have to do for the rest of the controller types.
I have found that I think I like the idea that the differential equations packages use, where they define problem types that contain the parameters for the problem, and then pass those into the solver functions. I think that would be a nice direction to move to, and possibly then also support a single design
function that each type overloads to design the controller. That provides a nice consistent interface (for instance, it removes the need for me to remember whether the function I wanted was lqr
, dlqr
, lqrd
, lqg
, etc. and their argument orders since I define everything when I create the problem instance).
Specifically I am thinking of an initial type hierarchy of:
Abstract type ControlProblem
--> Concrete type LQR
--> Concrete type LQG
Then for instance the LQR type has fields for sys, Q, R, S, P, N
, and then the synthesis functions have all the information they need inside this type to do the problem synthesis and return the controller matrix. The synthesis functions then take their sampling time from the included system (as is currently done by the overloaded lqr(sys, Q, R)
function).
@baggepinnen @mfalt @olof3 Thoughts?
I do like the idea in general, I'm wondering though, how many types would we need and how much would we save from the change?
If the type LQR
needs sys, Q, R, S, P, N
, what is the real difference of just calling the function lqr(sys, Q, R, S, P, N)
as opposed to design(LQR(sys, Q, R, S, P, N))
. I see the benefit in cases where the design has to be carried out a large number of times, but for the traditional user designing a single controller for a single system, I can't immediately see any benefit.
Somewhere you still need to make all the decisions and supply all the parameters, so you might shift the problem from remembering the function name to remembering a type name or a keyword name?
In general though, we have realized this issue before (hence the current LQG
type) and I would sure like to see some steps in this direction :)
Yes, while it may not save much for simple design patterns, there are cases where I think it will. For instance, if you have a continuous-time system and a continuous-time LQR problem for it but want a discrete-time controller. I am envisioning that you could then do
cProblem = LQR( contsys, contQ, contR, contS, etc. )
dProblem = c2d( cProblem ) # this could even be made an inplace operation, e.g. c2d!
K = design( dProblem )
This would transform the problem from the continuous one to the discrete one, doing the c2d for both the provided system and the cost matrices to ensure the problem is actually equivalent (see the introduction to [1] for how this would be done). Without the problem types, the c2d would need to return a tuple of all the matrices. (and yes, this actually is the equivalent to lqrd
in Matlab, but again, the naming of the functions isn't very helpful in my opinion).
Another place this could help is actually the work I do for predictive control. In there we do analysis using the problem parameters when analyzing the implementations, so the system and cost matrices need to be passed around to various functions for performing different types of analysis and design.
We don't necessarily have to replace the current functions - they could be left for compatibility and to help people switching from Matlab-esque languages - but I think the pattern of defining control problems can allow for cleaner expansion of the toolbox and a nicer interface.
[1] E. Bini and G. M. Buttazzo, ‘The Optimal Sampling Pattern for Linear Control Systems’, IEEE Transactions on Automatic Control, vol. 59, no. 1, pp. 78–90, 2014, doi: 10.1109/TAC.2013.2279913
I agree that the current LQG
might be doing too much to be useful.
I agree that it is a bit annoying with all the different arguments, but like @baggepinnen said, I think it would be hard to get rid of much of the problems.
Are you thinking about LQG or more general synthesis problems? For LQG don't think the situation is that bad. But I definitely see that it could be convenient to include a disturbance/noise model in the plant specification. It could also be nice to bundle signal limitations into a problem. However it seems difficult to provide something that is sufficiently general to be useful.
On a side note there are many issues that lead to confusion with the lqg functionality. The naming of the functions is horrible, to say the least. At least the one of lqrd
and dlqr
that handles sampled-data systems should be renamed to something like lqr_sd
. Also it would be a bit more symmetric with lqrc
and lqrd
. Also the cost and covariance matrices should perhaps be named something like Qx
or Qxx
, Qxu
, Quu
, Rww
, Rvv
or Rnn
So what you want to do seems to be
lqr(sys, Qxx, Quu, Qxu)
and then something like lqr_sd(sys, Ts, Qxx, Quu, Qxu)
(always tricky with the order of variables), or perhaps lqr_sd
should just take a discrete-time system and have the same arguments as lqr.
This functionality does not seems complicated enough to warrant the introduction of a new type.
At least the one of lqrd and dlqr that handles sampled-data systems should be renamed to something like lqr_sd.
Careful, dlqr
and lqrd
have different behavior. dlqr
assumes the system and matrices are already in discrete time when it does its discrete-time design, whereas lqrd
takes a continuous-time problem and does a discrete-time design after essentially doing a c2d
on it (this also converts the matrices into discrete-time).
However it seems difficult to provide something that is sufficiently general to be useful
Yes, trying to put everything inside a type is not feasible - and I don't advocate for making everything inside the type, but I think there is a large subset of the problem data that is relevant (for LQR, the parts I mentioned in the 1st comment seem to be the ones that are most revelevant). I am not too familiar with LQG design to be honest, but when I was drafting this proposal I saw the current implementation and felt it went a bit too far down the typing route. I am envisioning these problem types as a data container that allows for better portability of the parameters. For instance, here is a sample of generating an MPC problem in a toolbox I am working on (I am working on porting it to Julia right now):
# Create the cost function matrices
H = condensed_hessian_gen(A, B, Q, P, R, N);
G = condensed_linear_gen(A, B, Q, P, N);
L = compute_precond( A, B, Q, R, N )
As you can see, these functions require basically the same subset of problem data to operate on, which is what led me to want to create a type to hold the data.
While I could do this in my package, I was wanting to propose it for "upstream" so that there isn't a lot of fragmentation in the styles of packages in the developing control ecosystem. This can also be useful for providing benchmark/example problems in a nice concise way. Instead of giving the user a bunch of matrices in a specific order that they have to keep track of, they get the problem struct containing the data.
If you have such a type holding the relevant problem data (A, B, Q, P, R, N
in this case), feel free to submit a PR with the type and how you'd imagine it'd be used, possibly together with a translation from the the old style syntax to the new, .e.g.,
lqr(...) = design(LQR(...))
I find it easier to reason about a proposal when there is some code to look at, and the look-and-feel also also becomes a bit more clear :)
dlqr
andlqrd
have different behavior.
Yes, it is impossible to remember which one is which, so the naming is awful. The one that solves the sampled-data problem (discrete controller for continuous-time plant and cost function) should have a name that clearly indicates that it is doing so.