Skip to content

Commit 8c6c5ae

Browse files
docs: add doc page for FMU import capability
1 parent 57c79e9 commit 8c6c5ae

File tree

3 files changed

+226
-1
lines changed

3 files changed

+226
-1
lines changed

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
66
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
77
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
88
DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
9+
FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac"
10+
FMIZoo = "724179cf-c260-40a9-bd27-cccc6fe2f195"
911
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1012
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
1113
ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739"

docs/pages.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pages = [
1414
"tutorials/SampledData.md",
1515
"tutorials/domain_connections.md",
1616
"tutorials/callable_params.md",
17-
"tutorials/linear_analysis.md"],
17+
"tutorials/linear_analysis.md",
18+
"tutorials/fmi.md"],
1819
"Examples" => Any[
1920
"Basic Examples" => Any["examples/higher_order.md",
2021
"examples/spring_mass.md",

docs/src/tutorials/fmi.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Importing FMUs
2+
3+
ModelingToolkit is able to import FMUs following the [FMI Standard](https://fmi-standard.org/) versions 2 and 3.
4+
This integration is done through [FMI.jl](https://github.com/ThummeTo/FMI.jl) and requires importing it to
5+
enable the relevant functionality. Currently Model Exchange (ME) and CoSimulation (CS) FMUs are supported.
6+
Events, non-floating-point variables and array variables are not supported. Additionally, calculating the
7+
time derivatives of FMU states/outputs is not supported.
8+
9+
!!! danger "Experimental"
10+
11+
This functionality is currently experimental and subject to change without a breaking release of
12+
ModelingToolkit.jl.
13+
14+
## FMUs of full models
15+
16+
Here, we will demonstrate the usage of an FMU of an entire model (as opposed to a single component).
17+
First, the required libraries must be imported and the FMU loaded using FMI.jl.
18+
19+
```@example fmi
20+
using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq
21+
using ModelingToolkit: t_nounits as t, D_nounits as D
22+
23+
# This is a spring-pendulum FMU from FMIZoo.jl. It is a v2 FMU
24+
# and we are importing it in ModelExchange format.
25+
fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME)
26+
```
27+
28+
Following are the variables in the FMU (both states and parameters):
29+
30+
```@example fmi
31+
fmu.modelDescription.modelVariables
32+
```
33+
34+
Next, [`FMIComponent`](@ref) is used to import the FMU as an MTK component. We provide the FMI
35+
major version as a `Val` to the constructor, along with the loaded FMU and the type as keyword
36+
arguments.
37+
38+
```@example fmi
39+
@named model = ModelingToolkit.FMIComponent(Val(2); fmu, type = :ME)
40+
```
41+
42+
Note how hierarchical names in the FMU (e.g. `mass.m` or `spring.f`) are turned into flattened
43+
names, with `__` being the namespace separator (`mass__m` and `spring__f`).
44+
45+
!!! note
46+
47+
Eventually we plan to reconstruct a hierarchical system structure mirroring the one indicated
48+
by the variables in the FMU. This would allow accessing the above mentioned variables as
49+
`model.mass.m` and `model.spring.f` instead of `model.mass__m` and `model.spring__f` respectively.
50+
51+
Derivative variables such as `der(mass.v)` use the dummy derivative notation, and are hence transformed
52+
into a form similar to `mass__vˍt`. However, they can still be referred to as `D(model.mass__v)`.
53+
54+
```@example fmi
55+
equations(model)
56+
```
57+
58+
Since the FMI spec allows multiple names to alias the same quantity, ModelingToolkit.jl creates
59+
equations to alias them. For example, it can be seen above that `der(mass.v)` and `mass.a` have the
60+
same reference, and hence refer to the same quantity. Correspondingly, there is an equation
61+
`mass__vˍt(t) ~ mass__a(t)` in the system.
62+
63+
!!! note
64+
65+
Any variables and/or parameters that are not part of the FMU should be ignored, as ModelingToolkit
66+
creates them to manage the FMU. Unexpected usage of these variables/parameters can lead to errors.
67+
68+
```@example fmi
69+
defaults(model)
70+
```
71+
72+
All parameters in the FMU are given a default equal to their start value, if present. Unknowns are not
73+
assigned defaults even if a start value is present, as this would conflict with ModelingToolkit's own
74+
initialization semantics.
75+
76+
We can simulate this model like any other ModelingToolkit system.
77+
78+
```@repl fmi
79+
sys = structural_simplify(model)
80+
prob = ODEProblem(sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 5.0))
81+
sol = solve(prob, Tsit5())
82+
```
83+
84+
We can interpolate the solution object to obtain values at arbitrary time points in the solved interval,
85+
just like a normal solution.
86+
87+
```@repl fmi
88+
sol(0.0:0.1:1.0; idxs = sys.mass_a)
89+
```
90+
91+
FMUs following version 3 of the specification can be simulated with almost the same process. This time,
92+
we will create a model from a CoSimulation FMU.
93+
94+
```@example fmi
95+
fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :CS)
96+
@named inner = ModelingToolkit.FMIComponent(
97+
Val(3); fmu, communication_step_size = 0.001, type = :CS)
98+
```
99+
100+
This FMU has fewer equations, partly due to missing aliasing variables and partly due to being a CS FMU.
101+
CoSimulation FMUs are bundled with an integrator. As such, they do not function like ME FMUs. Instead,
102+
a callback steps the FMU at periodic intervals in time and obtains the updated state. This state is held
103+
constant until the next time the callback triggers. The periodic interval must be specified through the
104+
`communication_step_size` keyword argument. A smaller step size typically leads to less error but is
105+
more computationally expensive.
106+
107+
This model alone does not have any differential variables, and calling `structural_simplify` will lead
108+
to an `ODESystem` with no unknowns.
109+
110+
```@example fmi
111+
structural_simplify(inner)
112+
```
113+
114+
Simulating this model will cause the OrdinaryDiffEq integrator to immediately finish, and will not
115+
trigger the callback. Thus, we wrap this system in a trivial system with a differential variable.
116+
117+
```@example fmi
118+
@variables x(t) = 1.0
119+
@mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner])
120+
```
121+
122+
We can now simulate `sys`.
123+
124+
```@example fmi
125+
prob = ODEProblem(sys, [sys.inner.mass__s => 0.5, sys.inner.mass__v => 0.0], (0.0, 5.0))
126+
sol = solve(prob, Tsit5())
127+
```
128+
129+
The variables of the FMU are discrete, and their timeseries can be obtained at intervals of
130+
`communication_step_size`.
131+
132+
```@example fmi
133+
sol[sys.inner.mass__s]
134+
```
135+
136+
## FMUs of components
137+
138+
FMUs can also be imported as individual components. For this example, we will use custom FMUs used
139+
in the test suite of ModelingToolkit.jl.
140+
141+
```@example fmi
142+
fmu = loadFMU(joinpath("test", "fmi", "fmus", "SimpleAdder.fmu"); type = :ME)
143+
fmu.modelDescription.modelVariables
144+
```
145+
146+
This FMU is equivalent to the following model:
147+
148+
```julia
149+
@mtkmodel SimpleAdder begin
150+
@variables begin
151+
a(t)
152+
b(t)
153+
c(t)
154+
out(t)
155+
out2(t)
156+
end
157+
@parameters begin
158+
value = 1.0
159+
end
160+
@equations begin
161+
out ~ a + b + value
162+
D(c) ~ out
163+
out2 ~ 2c
164+
end
165+
end
166+
```
167+
168+
`a` and `b` are inputs, `c` is a state, and `out` and `out2` are outputs of the component.
169+
170+
```@repl fmi
171+
@named adder = ModelingToolkit.FMIComponent(Val(2); fmu, type = :ME);
172+
isinput(adder.a)
173+
isinput(adder.b)
174+
isoutput(adder.out)
175+
isoutput(adder.out2)
176+
```
177+
178+
ModelingToolkit recognizes input and output variables of the component, and attaches the appropriate
179+
metadata. We can now use this component as a subcomponent of a larger system.
180+
181+
```@repl fmi
182+
@variables a(t) b(t) c(t) [guess = 1.0];
183+
@mtkbuild sys = ODESystem(
184+
[adder.a ~ a, adder.b ~ b, D(a) ~ t,
185+
D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value],
186+
t;
187+
systems = [adder])
188+
equations(sys)
189+
```
190+
191+
Note how the output `adder.out` is used in an algebraic equation of the system. We have also given
192+
`sys.c` a guess, expecting it to be solved for by initialization. ModelingToolkit is able to use
193+
FMUs in initialization to solve for initial states. As mentioned earlier, we cannot differentiate
194+
through an FMU. Thus, automatic differentiation has to be disabled for the solver.
195+
196+
```@example fmi
197+
prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0],
198+
(0.0, 1.0), [sys.adder.value => 2.0])
199+
solve(prob, Rodas5P(autodiff = false))
200+
```
201+
202+
CoSimulation FMUs follow a nearly identical process. Since CoSimulation FMUs operate using callbacks,
203+
after triggering the callbacks and altering the discrete state the algebraic equations may no longer
204+
be satisfied. To resolve for the values of algebraic variables, we use the `reinitializealg` keyword
205+
of `FMIComponent`. This is a DAE initialization algorithm to use at the end of every callback. Since
206+
CoSimulation FMUs are not directly involved in the RHS of the system - instead operating through
207+
callbacks - we can use a solver with automatic differentiation.
208+
209+
```@example fmi
210+
fmu = loadFMU(joinpath("test", "fmi", "fmus", "SimpleAdder.fmu"); type = :CS)
211+
@named adder = ModelingToolkit.FMIComponent(
212+
Val(2); fmu, type = :CS, communication_step_size = 1e-3,
213+
reinitializealg = BrownFullBasicInit())
214+
@mtkbuild sys = ODESystem(
215+
[adder.a ~ a, adder.b ~ b, D(a) ~ t,
216+
D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value],
217+
t;
218+
systems = [adder])
219+
prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0],
220+
(0.0, 1.0), [sys.adder.value => 2.0])
221+
solve(prob, Rodas5P())
222+
```

0 commit comments

Comments
 (0)