ModelingToolkit.jl icon indicating copy to clipboard operation
ModelingToolkit.jl copied to clipboard

Only force unbound causal inputs to be states

Open ValentinKaisermayer opened this issue 2 years ago • 23 comments

https://github.com/SciML/ModelingToolkit.jl/blob/6b92d37c6f7cd57cb5d4036123f6e5787cc212a5/src/systems/connectors.jl#L51

Considering that input=true seems to force a variable to be a state, which is an undocumented behaviour and seems to change the behaviour of the model, I would suggest to change this to @debug for now.

ValentinKaisermayer avatar May 15 '22 18:05 ValentinKaisermayer

input=true forces a variable to be a state is intended. Otherwise users won't be able to change it in a callback.

YingboMa avatar May 17 '22 23:05 YingboMa

Ok, but why the warning? For instance in MTKSL the PID Block consists of about 10 blocks, each with inputs. The PID block itself has inputs. The state vector would be 10+ states, with a single PID block in the model. I do not think that this is what the user expects. On the other hand if the meta data is not assigned the warning is thrown, which also is not what a user of MTKSL will expect.

I can see the usefulness of the variable being in the state vector but it should be limited to very few cases and not be advertised as if it does not hurt.

ValentinKaisermayer avatar May 18 '22 04:05 ValentinKaisermayer

I think checking if physical connectors (with >=2 variables) to have flow and potential (voltage/current, temp./heat flow,...) is fine. It might be a good idea to add a type Potential for this. Modelica has strict rules for this. https://specification.modelica.org/master/connectors-and-connections.html https://specification.modelica.org/master/stream-connectors.html

A side note: the signal connectors of Modelica RealInput and RealOutput have no variable at all and simply can be used in place of a connector and of a variable. In MTKSL using the e.g. RealInput requires you to use input.u, which is more verbose than it needs to be.

ValentinKaisermayer avatar May 18 '22 05:05 ValentinKaisermayer

Can we add CausalInput and CausalOutput as type, like Flow and do the check of the connector based on the types? I think this is more consistent than having it as metadata. Also an input should not end up as state per se. If this is needed for changing an (external) input in a callback function, it is an implementation detail and has nothing to do with the modelling.

The following system has one state (from the integrator), however, with MTK and the inputs of the blocks annotated with input=true (otherwise the warning from above) it has five.

using ModelingToolkitStandardLibrary.Blocks
using ModelingToolkit, OrdinaryDiffEq

@parameters t

@named c = Constant(; k=2)
@named gain = Gain(1;)
@named int = Integrator(; k=1)
@named fb = Feedback(;)
@named model = ODESystem(
    [
        connect(c.output, fb.input1), 
        connect(fb.input2, int.output), 
        connect(fb.output, gain.input),
        connect(gain.output, int.input),
    ], 
    t, 
    systems=[int, gain, c, fb]
)
sys = structural_simplify(model)
julia> sys
Model model with 5 equations
States (5):
  int₊x(t) [defaults to 0.0]
  int₊input₊u(t) [defaults to 0.0]
  gain₊input₊u(t) [defaults to 0.0]
⋮
Parameters (3):
  int₊k [defaults to 1]
  gain₊k [defaults to 1]
  c₊k [defaults to 2]
Incidence matrix:5×6 SparseArrays.SparseMatrixCSC{Num, Int64} with 10 stored entries:
 ×  ⋅  ×  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ×
 ⋅  ×  ⋅  ⋅  ×  ⋅
 ⋅  ⋅  ⋅  ×  ×  ×
 ⋅  ⋅  ×  ×  ⋅  ⋅

ValentinKaisermayer avatar Jun 19 '22 20:06 ValentinKaisermayer

An even more grotesk example:

using ModelingToolkitStandardLibrary.Blocks
using ModelingToolkit, OrdinaryDiffEq

@parameters t

"""Second order demo plant"""
function Plant(;name, x_start=zeros(2))
    @named input = RealInput()
    @named output = RealOutput()
    D = Differential(t)
    sts = @variables x1(t)=x_start[1] x2(t)=x_start[2]
    eqs= [
        D(x1) ~ x2
        D(x2) ~ -x1 - 0.5 * x2 + input.u
        output.u ~ 0.9 * x1 + x2
    ]
    compose(ODESystem(eqs, t, sts, []; name), [input, output])
end

re_val = 1
@named ref = Constant(; k=re_val)
@named pid_controller = LimPID(k=3, Ti=0.5, Td=100, u_max=1.5, u_min=-1.5, Ni=0.1/0.5)
@named plant = Plant()
@named model = ODESystem(
    [
        connect(ref.output, pid_controller.reference), 
        connect(plant.output, pid_controller.measurement),
        connect(pid_controller.ctr_output, plant.input), 
    ], 
    t, 
    systems=[pid_controller, plant, ref]
)
sys = structural_simplify(model)
julia> states(sys)
24-element Vector{Term{Real, Base.ImmutableDict{DataType, Any}}}:
 pid_controller₊int₊x(t)
 pid_controller₊der₊x(t)
 plant₊x1(t)
 plant₊x2(t)
 pid_controller₊addP₊input1₊u(t)
 pid_controller₊addP₊input2₊u(t)
 pid_controller₊gainPID₊input₊u(t)
 pid_controller₊addPID₊input1₊u(t)
 pid_controller₊addPID₊input2₊u(t)
 pid_controller₊addPID₊input3₊u(t)
 pid_controller₊limiter₊input₊u(t)
 pid_controller₊addSat₊input1₊u(t)
 pid_controller₊addSat₊input2₊u(t)
 pid_controller₊gainTrack₊input₊u(t)
 pid_controller₊addI₊input1₊u(t)
 pid_controller₊addI₊input2₊u(t)
 pid_controller₊addI₊input3₊u(t)
 pid_controller₊int₊input₊u(t)
 pid_controller₊addD₊input1₊u(t)
 pid_controller₊addD₊input2₊u(t)
 pid_controller₊der₊input₊u(t)
 plant₊input₊u(t)
 pid_controller₊reference₊u(t)
 pid_controller₊measurement₊u(t)

ValentinKaisermayer avatar Jun 19 '22 20:06 ValentinKaisermayer

The detection of is_bound does not work for connectors, or rather, it works as intended but the notion of being bound does not fit with the use of the connector since the [input=true] variable will be connected to a variable in an outer namespace as soon as you have attached the connector to the host component

baggepinnen avatar Jun 21 '22 10:06 baggepinnen

Can we simply let the user do this and use types CausalInput and CausalOutput in a connector? This also would allow for more checks, see #1626. If the user wants to have access to a variable it can be forced into the state vector via [irreducible=true] (this is only ever needed for callback functions I think).

I'm not sure what the intended use-case for [input=true] is and why there is a need for [irreducible=true] as well. Maybe you can explain it to me.

One use-case I see is system linearization. If I think of how Simulink does this, you would take your model and annotate it with inputs and outputs. However, in MTK this is not needed. One can simply provide the reference to a variable linearize(sys, [in1, in2], [out1, out2]). I do not see why this should be part of the modelling.

ValentinKaisermayer avatar Jun 21 '22 14:06 ValentinKaisermayer

There are a few use cases I have in mind, the most important is that one should be able to call structural_simplify on systems that have inputs that are not connected as well as to generate code for systems that take the input as an argument, e.g.,

ẋ = dynamics(x, u, p, t)

One approach would be as you mention, to force the user to connect a CausalInput to a signal to explicitly indicate that this is an input. I'm not sure how the struct Flow type differs from metadata, but it's quite possible there are better ways of implementing it than the current approach.

baggepinnen avatar Jun 21 '22 14:06 baggepinnen

the most important is that one should be able to call structural_simplify on systems that have inputs that are not connected

What do you mean? What outcome do you expect from that?

ValentinKaisermayer avatar Jun 21 '22 14:06 ValentinKaisermayer

Some mechanism to denote inputs is required to call structural simplification, otherwise it's an error due to a mismatch between the number of equations and the number of states. Structural simplification is required as a step towards both linearization and code generation with inputs as arguments

baggepinnen avatar Jun 21 '22 15:06 baggepinnen

You mean external inputs?

ValentinKaisermayer avatar Jun 21 '22 15:06 ValentinKaisermayer

Could it be an option to structural_simplify then?

ValentinKaisermayer avatar Jun 21 '22 15:06 ValentinKaisermayer

Yes, I have implicitly assumed external inputs all along, but what is an external input depends on if something is connected or not. This is the reason the is_bound functionality exists, to figure out if an input is external or not. The way it's implemented fails when the input belong to a special RealInput system since then it will technically be connected to something outside even if the connector is not used.

I am not married to the current approach, I'd happily take any improvement to what we have now. I do like to have a mechanism that figures out if an input is external (not connected) automatically, since manually specifying it makes for a lot of redefinition of models. For example, if I call

sys = get_fancy_io_model()

I'd like to be able to either linearize sys, or connect sys.input to othersys.output without having to create any intermediate

temp = ODESystem(glue_equation, systems=[sys])

when linearizing, sys.input is an external input, but when connecting sys.input to othersys.output, it stops being external.

Forcing the user to manually specify the inputs when calling linearize is not ideal since then the user can not, for instance, automate the linearization of several different IO models since each call would require figuring out the names of the inputs. What's required for that is that MTK knows what's an external input without specification from the user

baggepinnen avatar Jun 21 '22 15:06 baggepinnen

If we just think about a component-based model it is easy. All unconnected connectors are used for linearization. If you want to connect the system you can do that also.

ValentinKaisermayer avatar Jun 21 '22 15:06 ValentinKaisermayer

On a higher level, knowing that a system has a causal input-output relationship is really useful, for instance, one can design much nicer interfaces than when this knowledge is lacking. Consider the extreme case

feedback(P*C)

vs.

ref = RealInput()
add = Add([1, -1])
output = RealOutput()
connect(P.output, add.input1, output)
connect(ref.output, add.input2)
connect(C.output, P.input)
connect(add.output, C.input)

The two code snippets achieve the same thing, the first one knowing that P and C are IO systems and the second one with the MTK approach. Both form the following interconnection

     ┌─────┐     ┌─────┐
r    │     │  u  │     │
──+─►│  C  ├────►│  P  ├─┬─►
 -│  │     │     │     │ │
  │  └─────┘     └─────┘ │
  │                      │
  └──────────────────────┘

Having an input be a connector sounds like a perfectly fine approach to me.

baggepinnen avatar Jun 21 '22 15:06 baggepinnen

In general, I am in favor of inputs being marked by specific labels, as is done in Modelica (i.e. the input tag); forcing these inputs to be specified as connectors will require the user to build or add specialized connectors, which is a much heavier requirement.

crlaugh avatar Jun 21 '22 17:06 crlaugh

The input tag has different meanings depending on the environment used in. https://build.openmodelica.org/Documentation/ModelicaReference.'input'.html

ValentinKaisermayer avatar Jun 21 '22 17:06 ValentinKaisermayer

If we just think about a component-based model it is easy. All unconnected connectors are used for linearization. If you want to connect the system you can do that also.

WSM uses the same approach, see here. image

ValentinKaisermayer avatar Jun 22 '22 08:06 ValentinKaisermayer

@baggepinnen can this be closed?

ValentinKaisermayer avatar Sep 12 '22 20:09 ValentinKaisermayer

I think so, we now have the io_processing function and are considering implementing something akin to the "analysis points" that are available in simulink

baggepinnen avatar Sep 13 '22 06:09 baggepinnen

So the PID controller example does not have 24 states anymore? 😅

ValentinKaisermayer avatar Sep 13 '22 06:09 ValentinKaisermayer

I don't know, I thought you had checked and wanted to close the issue

baggepinnen avatar Sep 13 '22 06:09 baggepinnen

I will check it.

ValentinKaisermayer avatar Sep 13 '22 08:09 ValentinKaisermayer

No still the same and every variable that is marked as input ends up as a state. I find this pretty unacceptable.

ValentinKaisermayer avatar Oct 01 '22 18:10 ValentinKaisermayer

Maybe the best solution for now is to remove the [input = true] annotations in the Blocks library and rely on the explicit input passing that is used for linearize and io_preprocessing. If we get more formally specified semantics for causal variables at some point we ca reintroduce the annotations.

baggepinnen avatar Oct 02 '22 14:10 baggepinnen