Skip to content

Commit c412cd7

Browse files
Merge pull request #3405 from AayushSabharwal/as/fmi-doc
docs: add doc page for FMU import capability
2 parents 3ea38a9 + 77774ae commit c412cd7

File tree

4 files changed

+234
-3
lines changed

4 files changed

+234
-3
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/examples/remake.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,10 @@ using SymbolicIndexingInterface
9898
optfn = OptimizationFunction(loss, Optimization.AutoForwardDiff())
9999
# function to set the parameters we are optimizing
100100
setter = setp(odeprob, [α, β, γ, δ])
101-
# `DiffCache` to avoid allocations
102-
diffcache = DiffCache(canonicalize(Tunable(), parameter_values(odeprob))[1])
101+
# `DiffCache` to avoid allocations.
102+
# `copy` prevents the buffer stored by `DiffCache` from aliasing the one in
103+
# `parameter_values(odeprob)`.
104+
diffcache = DiffCache(copy(canonicalize(Tunable(), parameter_values(odeprob))[1]))
103105
# parameter object is a tuple, to store differently typed objects together
104106
optprob = OptimizationProblem(
105107
optfn, rand(4), (odeprob, timesteps, data, setter, diffcache),

docs/src/tutorials/fmi.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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(
143+
joinpath(@__DIR__, "..", "..", "..", "test", "fmi", "fmus", "SimpleAdder.fmu");
144+
type = :ME)
145+
fmu.modelDescription.modelVariables
146+
```
147+
148+
This FMU is equivalent to the following model:
149+
150+
```julia
151+
@mtkmodel SimpleAdder begin
152+
@variables begin
153+
a(t)
154+
b(t)
155+
c(t)
156+
out(t)
157+
out2(t)
158+
end
159+
@parameters begin
160+
value = 1.0
161+
end
162+
@equations begin
163+
out ~ a + b + value
164+
D(c) ~ out
165+
out2 ~ 2c
166+
end
167+
end
168+
```
169+
170+
`a` and `b` are inputs, `c` is a state, and `out` and `out2` are outputs of the component.
171+
172+
```@repl fmi
173+
@named adder = ModelingToolkit.FMIComponent(Val(2); fmu, type = :ME);
174+
isinput(adder.a)
175+
isinput(adder.b)
176+
isoutput(adder.out)
177+
isoutput(adder.out2)
178+
```
179+
180+
ModelingToolkit recognizes input and output variables of the component, and attaches the appropriate
181+
metadata. We can now use this component as a subcomponent of a larger system.
182+
183+
```@repl fmi
184+
@variables a(t) b(t) c(t) [guess = 1.0];
185+
@mtkbuild sys = ODESystem(
186+
[adder.a ~ a, adder.b ~ b, D(a) ~ t,
187+
D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value],
188+
t;
189+
systems = [adder])
190+
equations(sys)
191+
```
192+
193+
Note how the output `adder.out` is used in an algebraic equation of the system. We have also given
194+
`sys.c` a guess, expecting it to be solved for by initialization. ModelingToolkit is able to use
195+
FMUs in initialization to solve for initial states. As mentioned earlier, we cannot differentiate
196+
through an FMU. Thus, automatic differentiation has to be disabled for the solver.
197+
198+
```@example fmi
199+
prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0],
200+
(0.0, 1.0), [sys.adder.value => 2.0])
201+
solve(prob, Rodas5P(autodiff = false))
202+
```
203+
204+
CoSimulation FMUs follow a nearly identical process. Since CoSimulation FMUs operate using callbacks,
205+
after triggering the callbacks and altering the discrete state the algebraic equations may no longer
206+
be satisfied. To resolve for the values of algebraic variables, we use the `reinitializealg` keyword
207+
of `FMIComponent`. This is a DAE initialization algorithm to use at the end of every callback. Since
208+
CoSimulation FMUs are not directly involved in the RHS of the system - instead operating through
209+
callbacks - we can use a solver with automatic differentiation.
210+
211+
```@example fmi
212+
fmu = loadFMU(
213+
joinpath(@__DIR__, "..", "..", "..", "test", "fmi", "fmus", "SimpleAdder.fmu");
214+
type = :CS)
215+
@named adder = ModelingToolkit.FMIComponent(
216+
Val(2); fmu, type = :CS, communication_step_size = 1e-3,
217+
reinitializealg = BrownFullBasicInit())
218+
@mtkbuild sys = ODESystem(
219+
[adder.a ~ a, adder.b ~ b, D(a) ~ t,
220+
D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value],
221+
t;
222+
systems = [adder])
223+
prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0],
224+
(0.0, 1.0), [sys.adder.value => 2.0])
225+
solve(prob, Rodas5P())
226+
```

0 commit comments

Comments
 (0)