ModelingToolkit.jl
ModelingToolkit.jl copied to clipboard
Only force unbound causal inputs to be states
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.
input=true
forces a variable to be a state is intended. Otherwise users won't be able to change it in a callback.
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.
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.
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:
× ⋅ × ⋅ ⋅ ⋅
⋅ ⋅ ⋅ ⋅ ⋅ ×
⋅ × ⋅ ⋅ × ⋅
⋅ ⋅ ⋅ × × ×
⋅ ⋅ × × ⋅ ⋅
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)
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
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.
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.
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?
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
You mean external inputs?
Could it be an option to structural_simplify
then?
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
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.
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.
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.
The input tag has different meanings depending on the environment used in. https://build.openmodelica.org/Documentation/ModelicaReference.'input'.html
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.
@baggepinnen can this be closed?
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
So the PID controller example does not have 24 states anymore? 😅
I don't know, I thought you had checked and wanted to close the issue
I will check it.
No still the same and every variable that is marked as input ends up as a state. I find this pretty unacceptable.
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.