MTK: @parameters vs. kwargs -- what is good practice?

What is good practice when it comes to use of @parameters and kwargs in “component types”?

To make my question concrete, consider the following “component type” (is that the proper term? – this is not meant as a “serious” component, just a simple example):

# Component type for open tank water level model
#
@component function Tank(; name, rho=1, A=5, md_c=25, p_s=1e4, eps_v=1e-6)
    # Computing numeric parameters prior to introducing symbolic parameters
    # Initial mass
    m0 = 1.5*rho*A
    # Symbolic model parameters
    params = @parameters begin 
        rho=rho,    [unit=u"kg/L", description = "Liquid density"]
        A=A,        [unit=u"m^2", description = "Cross sectional tank area"]
        md_c=md_c,  [unit=u"kg/s", description = "Effluent valve capacity"]
        p_s=p_s,    [unit=u"dPa", description = "Scaling pressure in valve model"]
        g = 98.1,   [unit=u"dm/s^2", description = "Gravitational acceleration"]
    end
    # Symbolic model variables, with initial values needed
    vars = @variables begin
        m(t)=m0,    [unit=u"kg", description = "Liquid mass"]
        md_i(t),    [unit=u"kg/s", description = "Influent mass flow rate"]
        md_e(t),    [unit=u"kg/s", description = "Effluent mass flow rate"]
        V(t),       [unit=u"L", description = "Liquid volume"]
        h(t),       [unit=u"dm", description = "level"]
        Dp(t),      [unit=u"dPa", description = "Pressure drop across valve"]
    end
    # Model equations
    eqs = [
        D(m) ~ md_i-md_e
        m ~ rho*V
        V ~ A*h
        Dp ~ rho*g*h
        md_e ~ md_c*soft_sqrtabs(Dp/p_s; eps_v=eps_v)
    ]
    #
    return System(eqs, t, vars, params; name = name)
end

My rationale for the above:

  1. I only transfer parameters via kwargs that the user should be able to choose.
  2. Specifically, the user should not need to bother about natural constants (e.g., g) – what in Modelica is a constant. So I lean towards not including such values as kwargs.
  3. I introduce @parameters so that the model, when listed by MTK, contains parameter symbols, and not numeric values.

My questions are related to – what is good practice when it comes to:

a. Names of kwargs. Observe that I have used the same identifier for the kwargs (numeric variables) and the symbolic variables in the @parameters block. This is probably not good practice. To me, it makes sense to change the kwargs and not the symbolic parameters.

  • What is a good, systematic convention for naming the kwargs? Google’s AI tool suggests “parameter_val”, e.g., A_val, rho_val, etc. To me, this looks “clumsy” – I prefer simpler notation. I could use _A, _rho, etc., or A_, rho_, etc., but I don’t know whether that is “nice” as kwargs. I could use p_A, p_rho or A_p, rho_p (“p” for parameter"). Etc.
  • Any other systematic choices that makes sense?

b. My idea is to only use this special notation of kwargs (p_A, or what not) for “model” parameters, i.e., parameters that have similar symbolic names in the @parameters block. So:

  • I do not see the need to have symbolic parameters for purely numeric numbers such as eps_v, which is an accuracy parameter in the indicated soft_sqrtabs function (essentially a soft approximation to sqrt(abs(x)))
  • Likewise, I would not use a symbolic parameter for, say the number N of discretization cells in PDEs, etc.

Suggestions for good practice are welcome! Key to me is:

  • It should be systematic, which makes it simpler for the user, and
  • It should be simple to maintain and not break any Julia conventions, and
  • It should make sense in the MTK eco system

[Observe that my example “component type” above only works properly if initial mass m0 at the above location, i.e., prior to the @parameters block. If I put the definition of m0 after the @parameters block, this makes m0 symbolic instead of numeric, and messes up the possibility to properly initialize the system. This messing up would disappear if I use different names for symbolic parameters and kwargs.]

In my opinion kwargs for parameters is not a good idea. Please see: Home · ModelingToolkitParameters.jl

It also appears that you are modeling fluids, so a domain may help so you can propagate fluid properties: Domains · ModelingToolkit.jl

Finally, I’d recommend a globally scoped g for gravity Model building reference · ModelingToolkit.jl

OK – but the page you refer to is outdated? @mtkbuild sys = Motor(k=1, r=2, l=3) is not valid construction any more, I think. The “modern” syntax is (??):

@named sys = Motor(k=1, r=2, l=3)

That begs the question: is @mtkparams also outdated? I get an error message when I try to use it, so the documentation is not very helpful.

Oh my mistake, the @mtkbuild should have been updated, hard to keep up with the fast paced MTK development :slight_smile: , I just pushed that change.

But to answer your question, no ModelingToolkitParameters.jl is being actively developed and maintained, it is very much current. I recommend you try it out, I believe it will resolve your questions about keyword arguments.

OK. I guess it takes some time before your pushed update appears?

Is ModelingToolkitParameters.jl imported with ModelingToolkit, or is this a package that needs separate import?

And, I guess I should add these questions/comments…

  • Parameters such as eps_v in my “component type” – I don’t see any reason why this should be made a symbolic parameters (unless it shows up in the MTK model presentation). Package ModelingToolkitParameters.jl - does that one only work with symbolic parameters.

  • A few years back, I was playing around with Symbolics, and figured out how I could convert PDEs to a set of ODEs using the weighted residual method. But my method only worked when I used global basis functions (I used polynomials, and collocation weights). Reason: if I used local basis functions and provided the range of the support for the various functions as symbolic variables in Symbolics, I could not use if statements.

  • It would perhaps be possible to do this with the current ifelse function, but I think it is more flexible to use numeric parameters. If ModelingToolkitParameters.jl only support symbolic parameters, that would make it more complex to set up an automatic discretization of a PDE within a component type?? Of course, for that case, I could add N (number of local support regions) as a kwarg, I guess – if all my copies of this component instance use the same number N, there is no overhead if I use this for the first one, and the ModelingToolkitParameters.jl package to set the parameters for the symbolic parameters??

  • So far, I have used kwargs. Then I document these in the docstring of the “component type”. What is a proper way to document these when I use ModelingToolkitParameters.jl? Your package seems to print a tree structure of the parameters. Is it possible to also print the “description” string?

It’s a separate import, so you’ll need:

using ModelingToolkit
using ModelingToolkitParameters

Hm.you are referring to two different web pages for ModelingToolkitParameters.jl. The first one you referred to (and which works) is in your first response, while the second one (“But to answer your question, no ModelingToolkitParameters.jl is being actively developed”) points to a page without much content.

My two “concerns” are, probably:

  • How to document parameters when they are not kwargs – I need to be able to document to a user how the user is supposed to use the component. What is a good way to do that when there is no apparent kwarg?
  • In my current “very alpha-level” version of a library, I rely heavily on splatting named tuples into component types to set parameters in the component instances. How would that work in conjunction with ModelingToolkitParameters.jl?

The link you are referring to is autogenerated by discourse.julialang.org, it’s pointing to the package repository, you’ll see the link to the docs in the About section.

My general practice with MTK is if a value can be adjusted by the user, it should be a parameter, if it’s a constant, like you mentioned gravity for example, then this can be a literal Julia constant, and not defined inside of @parameters. So you can put const g=9.807 in your Julia package and then g is a Julia variable and not a symbolic parameter

Each parameter can have a description, as you’ve already implemented, therefore components are self documenting in this way, as descriptions are used when the component is displayed.

Finally, as I write in my docs for ModelingToolkitParameters, using kwargs is only useful if you are going to only instantiate the model once and never leverage the symbolic parameters to make adjustments. You mention splatting kwargs, which I suspect is happening because you are propagating set parameters through higher level components to lower level components. In my opinion this is not the right way to do this because:
a) this is only useful for setting the initial model instance, and not used when you want to change the parameters
b) means you need to continuously propagate values that should be exposed further up the model chain, which all leads to a very difficult system to maintain, with little value

Instead, I recommend you only define parameters where things should be adjusted and let ModelingToolkitParameters automatically provide you the mechanism to set default values for the initial model instance and provide you a simple parameter object that can be used to easy make parameter changes.

So for example, in “./examples/ActiveSuspension” there is a model (ActiveSuspensionModel) defined with no kwargs. I can build this model using all defined defaults like this…

@mtkcompile sys = ActiveSuspensionModel.Model()
prob = ODEProblem(sys, [], (0, 10))
sol = solve(prob)

Or I can instantiate a new instance with different default values, say I want to set the proportional gain of the pid controller…

@mtkcompile sys = ActiveSuspensionModel.Model()
@mtkparams sys_pars = ActiveSuspensionModel.Model(pid=ActiveSuspensionModel.Controller(kp=100))
prob = ODEProblem(sys, pmap(sys, sys_pars), (0, 10))
sol = solve(prob)

Or if I want to now adjust the parameter (and not instantiate a whole new instance of the model), I can do this…

sys_pars.pid.kd = 200.0
prob′ = remake(prob; p = pmap(sys, sys_pars))
sol = solve(prob′)

I agree, in principle. However, I think this will insert the value (9.807) into the model when doing equations(sys), and therefore look a little fuzzy.

I use a design function to design geometric properties, etc. of the system, and splat these into the component constructor. These geometric properties are probably not going to change (lengths, diameters, volumes, etc.). So I think it is ok to specify them in the constructor as kwargs.

However, the design function bases these numbers on, e.g., gas fraction in the fluid, friction factors, leakage parameters, etc., which I also splat into the constructor, but which are candidates for model fitting. So some of the values I splat could probably be candidates for ModelingToolkitParameters.