JuliaControl / ControlSystems.jl

A Control Systems Toolbox for Julia
https://juliacontrol.github.io/ControlSystems.jl/stable/
Other
503 stars 85 forks source link

Add feature to create a transfer-function from crude expression #341

Closed KronosTheLate closed 3 years ago

KronosTheLate commented 3 years ago

Inspired by MatLab, I really want to be able to create a method for tf() that takes a crude expression, simplifies it and extracts the polynomial coefficient for the numerator and denominator all on its own. I did not want to make the nessecary dependency on SymPy.jl, but it was the only candidate for a package to do what I wanted. This would optimally be changed to something pure Julia IMO, as I think that adding a dependency on SymPy.jl is a big dependancy (right?). Another solution that I am not sure if is a feasable solution, is that the method for tf() can be defined in this package, and then the method's documentation and errors can instruct the user to use SymPy.jl seperatly. So this is still very much a brainstorm-phase. BUT I do have a working prototype. It requires that the user has defined their expression for the transfer-function as a SymPy.jl type Sym, with only a single variable s (lower-case). But if the user does this, I am pretty sure that the following code will do the rest in order to define a transfer-function from it:

using SymPy
import ControlSystems.tf

@vars s
R = 10
R_c = 1e-3
C = 150e-6
L = 47e-3
Z_eq = 1/(1/R+1/(1/s*C+R_c))
test_expr = Z_eq/(Z_eq+s*L)

"""
This function takes an expression defined with "s" as a symbolic value,
as defined by SymPy, and returns a transfer-function.
"""
function tf(expr::Sym)
    simple_expr = expr |> simplify
    num, den = expand.(simple_expr.as_numer_denom())
    numerator_degree_as_Sym    = degree(num, gen=s)
    denominator_degree_as_Symb = degree(den, gen=s)

    """
    This function is nessecary because the return value from degree() is of type 
    symbol, which can not be used to create a range as is later needed.
    But the value can be compared, so this is how I extract the value as a Julia-typed variable
    """
    function SymolicNumber_to_Int(expr)
        for i ∈ 1:100
            if expr==i return i end
        end
    end

    degree_num = SymolicNumber_to_Int(numerator_degree_as_Sym)
    degree_den = SymolicNumber_to_Int(denominator_degree_as_Symb)

    function find_coeffs(expr, degree)
        coeffs = Vector{Float64}()
        for i in 0:degree
            pushfirst!(coeffs, expr.coeff(s, i))
        end
        return coeffs
    end

    final_numerator = find_coeffs(num, degree_num)
    final_denominator = find_coeffs(den, degree_den)

    return tf(final_numerator, final_denominator)
end

Now this function is not tested to be reliable at all, and I do have a weird result from defining giving it the expression (1/(s*C)+R_c)/(Z_eq*(R+R_c+1/s/C)), where this is defined:


R = 10
R_c = 1e-3
C = 150e-6
L = 47e-3
Z_eq = 1/(1/R+1/(1/s*C+R_c))```
I am expecting 1 zero and two poles, but I am getting two zeros and one pole instead. But I think that could be fixed by taking more care in using SymPy.jl to simplify the expression (Or maybe it is correct, I am not sure).

I know that the code is far from pretty, it is a first prototype I knocked out in a hurry. But is it a good idea? Is this approach doable?  I would LOVE this feature personally, and I am motivated to make it happen
olof3 commented 3 years ago

I am not sure that I understand what you want to do.

Could it be that something like

s = tf("s")
R = 10
R_c = 1e-3
C = 150e-6
L = 47e-3
Z_eq = 1/(1/R+1/(1/s*C+R_c))
test_expr = Z_eq/(Z_eq+s*L)

is what you are looking for?

mfalt commented 3 years ago

Edit: Seems like ofof3 beat me to it.

Is the goal to make sure that the resulting rational function is of minimal degree, and a numerical minreal is not sufficient? PS: I think the feedback function can be used to avoid some of the extra poles/zeros instead of /

baggepinnen commented 3 years ago

I have defined a similar functions doing symbolic manipulations using sympy and providing conversions to and from numerical transfer functions, it's quite handy since mineral is quite terrible and polynomial calculations are numerically iffy. Sympy is not a nice dependency so we do not want to add that, but it could definitely live in its own package.

It's also nice to have symbolic expressions together with latexify support :)

KronosTheLate commented 3 years ago

I am not sure that I understand what you want to do.

Could it be that something like

s = tf("s")
R = 10
R_c = 1e-3
C = 150e-6
L = 47e-3
Z_eq = 1/(1/R+1/(1/s*C+R_c))
test_expr = Z_eq/(Z_eq+s*L)

is what you are looking for?

YES, this is prececly the feature I was trying to recreate!! Is there anything about it in the documentation?

Is the goal to make sure that the resulting rational function is of minimal degree, and a numerical minreal is not sufficient? PS: I think the feedback function can be used to avoid some of the extra poles/zeros instead of /

The goal is just what olof3 said, it looks perfectly analogous to the MatLab-function that showed me that this was possible. I don't really know what a "numerical minreal means...

I have defined a similar functions doing symbolic manipulations using sympy and providing conversions to and from numerical transfer functions, it's quite handy since mineral is quite terrible and polynomial calculations are numerically iffy. Sympy is not a nice dependency so we do not want to add that, but it could definitely live in its own package.

Are you saying you have your own SymPy version of this? What does it do that can not be done with s = tf("s")?.

It's also nice to have symbolic expressions together with latexify support :)

Sure is

So I guess I will go ahead and try to improve the docs, as I don't see this mentioned under "creating_systems.md", nor do I see it in the description for tf().

I would also like to change the name of the function agument Tf to duration and Ts to size_timestep, just to make the function more self-documenting. So if you guys don't disagree with changing argument-names, I will most likely make that change if/when I add documentation.

Thanks for the quick responses, and I am psyched that this feature is already included. Now I am just going to should it from a mountain-top under the docs for creating transfer-function ^_^

baggepinnen commented 3 years ago

Are you saying you have your own SymPy version of this? What does it do that can not be done with s = tf("s")?.

It's symbolic as opposed to numeric, it thus provides unlimited precision and greater intuition. Transfer function can hold symbolic variables as is, but many sympy functions work better on purely symbolic expressions, hence the extra machinery to go back and forth.

I would also like to change the name of the function agument Tf to duration and Ts to size_timestep, just to make the function more self-documenting. So if you guys don't disagree with changing argument-names, I will most likely make that change if/when I add documentation.

Changing names will break peoples code and in this case I do not think it's warranted. We've had extensive discussions on the naming of arguments and I do not think we are prepared to change anything again. We have chosen names that are fairly close to the matlab names and also variable names commonly used in the literature. Good variables names is subjective and I do not think it will be possible to accommodate everyone's favorite name, but it's easy enough to document them.

KronosTheLate commented 3 years ago

Changing names will break peoples code and in this case I do not think it's warranted. We've had extensive discussions on the naming of arguments and I do not think we are prepared to change anything again. We have chosen names that are fairly close to the matlab names and also variable names commonly used in the literature. Good variables names is subjective and I do not think it will be possible to accommodate everyone's favorite name, but it's easy enough to document them.

As long as it is not a kwarg, changing the name of the argument does not change anything, does it? If it does, please tell me how, because I don't understand.

But point taken. I will simply document them, and not change any argument-name

Are you saying you have your own SymPy version of this? What does it do that can not be done with s = tf("s")?.

It's symbolic as opposed to numeric, it thus provides unlimited precision and greater intuition. Transfer function can hold symbolic variables as is, but many sympy functions work better on purely symbolic expressions, hence the extra machinery to go back and forth.

Nice. I think the current functionality is sufficient for me, but maybe when Julia gets a good, pure-julia symbolic package with the nessecary functionality, everything related to symbolic transfer-functions can be included in the package ^_^

baggepinnen commented 3 years ago

As long as it is not a kwarg, changing the name of the argument does not change anything, does it? If it does, please tell me how, because I don't understand.

No that's true, the names of positional arguments is mostly an internal thing which is not part of the API.

baggepinnen commented 3 years ago

My symbolic utilities have now taken shape and are available here https://github.com/baggepinnen/SymbolicControlSystems.jl