Creation and training of ME-NeuralFMUs
Tutorial by Johannes Stoljar, Tobias Thummerer
Last edit: 15.11.2023
License
# Copyright (c) 2021 Tobias Thummerer, Lars Mikelsons, Johannes Stoljar
# Licensed under the MIT license.
# See LICENSE (https://github.com/thummeto/FMIFlux.jl/blob/main/LICENSE) file in the project root for details.
Motivation
The Julia Package FMIFlux.jl is motivated by the application of hybrid modeling. This package enables the user to integrate his simulation model between neural networks (NeuralFMU). For this, the simulation model must be exported as FMU (functional mock-up unit), which corresponds to a widely used standard. The big advantage of hybrid modeling with artificial neural networks is, that effects that are difficult to model (because they might be unknown) can be easily learned by the neural networks. For this purpose, the NeuralFMU is trained with measurement data containing the not modeled physical effect. The final product is a simulation model including the originally not modeled effects. Another big advantage of the NeuralFMU is that it works with little data, because the FMU already contains the characteristic functionality of the simulation and only the missing effects are added.
NeuralFMUs do not need to be as easy as in this example. Basically a NeuralFMU can combine different ANN topologies that manipulate any FMU-input (system state, system inputs, time) and any FMU-output (system state derivative, system outputs, other system variables). However, for this example a NeuralFMU topology as shown in the following picture is used.
NeuralFMU (ME) from [1].
Introduction to the example
In this example, simplified modeling of a one-dimensional spring pendulum (without friction) is compared to a model of the same system that includes a nonlinear friction model. The FMU with the simplified model will be named simpleFMU in the following and the model with the friction will be named realFMU. At the beginning, the actual state of both simulations is shown, whereby clear deviations can be seen in the graphs. The realFMU serves as a reference graph. The simpleFMU is then integrated into a NeuralFMU architecture and a training of the entire network is performed. After the training the final state is compared again to the realFMU. It can be clearly seen that by using the NeuralFMU, learning of the friction process has taken place.
Target group
The example is primarily intended for users who work in the field of first principle and/or hybrid modeling and are further interested in hybrid model building. The example wants to show how simple it is to combine FMUs with machine learning and to illustrate the advantages of this approach.
Other formats
Besides, this Jupyter Notebook there is also a Julia file with the same name, which contains only the code cells and for the documentation there is a Markdown file corresponding to the notebook.
Getting started
Installation prerequisites
Description | Command | |
---|---|---|
1. | Enter Package Manager via | ] |
2. | Install FMI via | add FMI |
3. | Install FMIFlux via | add FMIFlux |
4. | Install FMIZoo via | add FMIZoo |
5. | Install DifferentialEquations via | add DifferentialEquations |
6. | Install Plots via | add Plots |
7. | Install Random via | add Random |
Code section
To run the example, the previously installed packages must be included.
# imports
using FMI
using FMIFlux
using FMIFlux.Flux
using FMIZoo
using DifferentialEquations: Tsit5
import Plots
# set seed
import Random
Random.seed!(42);
[33m[1m┌ [22m[39m[33m[1mWarning: [22m[39mError requiring `Enzyme` from `LinearSolve`
[33m[1m│ [22m[39m exception =
[33m[1m│ [22m[39m LoadError: ArgumentError: Package LinearSolve does not have Enzyme in its dependencies:
[33m[1m│ [22m[39m - You may have a partially installed environment. Try `Pkg.instantiate()`
[33m[1m│ [22m[39m to ensure all packages in the environment are installed.
[33m[1m│ [22m[39m - Or, if you have LinearSolve checked out for development and have
[33m[1m│ [22m[39m added Enzyme as a dependency but haven't updated your primary
[33m[1m│ [22m[39m environment's manifest file, try `Pkg.resolve()`.
[33m[1m│ [22m[39m - Otherwise you may need to report an issue with LinearSolve
[33m[1m│ [22m[39m Stacktrace:
[33m[1m│ [22m[39m [1] [0m[1mmacro expansion[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4mloading.jl:1167[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [2] [0m[1mmacro expansion[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4mlock.jl:223[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [3] [0m[1mrequire[22m[0m[1m([22m[90minto[39m::[0mModule, [90mmod[39m::[0mSymbol[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:1144[24m[39m
[33m[1m│ [22m[39m [4] [0m[1minclude[22m[0m[1m([22m[90mmod[39m::[0mModule, [90m_path[39m::[0mString[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mBase.jl:419[24m[39m
[33m[1m│ [22m[39m [5] [0m[1minclude[22m[0m[1m([22m[90mx[39m::[0mString[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[35mLinearSolve[39m [90mC:\Users\runneradmin\.julia\packages\LinearSolve\qCLK7\src\[39m[90m[4mLinearSolve.jl:1[24m[39m
[33m[1m│ [22m[39m [6] [0m[1mmacro expansion[22m
[33m[1m│ [22m[39m [90m @ [39m[90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mRequires.jl:40[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [7] top-level scope
[33m[1m│ [22m[39m [90m @ [39m[90mC:\Users\runneradmin\.julia\packages\LinearSolve\qCLK7\src\[39m[90m[4minit.jl:16[24m[39m
[33m[1m│ [22m[39m [8] [0m[1meval[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4mboot.jl:368[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [9] [0m[1meval[22m
[33m[1m│ [22m[39m [90m @ [39m[90mC:\Users\runneradmin\.julia\packages\LinearSolve\qCLK7\src\[39m[90m[4mLinearSolve.jl:1[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [10] [0m[1m(::LinearSolve.var"#88#97")[22m[0m[1m([22m[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[35mLinearSolve[39m [90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mrequire.jl:101[24m[39m
[33m[1m│ [22m[39m [11] [0m[1mmacro expansion[22m
[33m[1m│ [22m[39m [90m @ [39m[90m[4mtiming.jl:382[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [12] [0m[1merr[22m[0m[1m([22m[90mf[39m::[0mAny, [90mlistener[39m::[0mModule, [90mmodname[39m::[0mString, [90mfile[39m::[0mString, [90mline[39m::[0mAny[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[36mRequires[39m [90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mrequire.jl:47[24m[39m
[33m[1m│ [22m[39m [13] [0m[1m(::LinearSolve.var"#87#96")[22m[0m[1m([22m[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[35mLinearSolve[39m [90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mrequire.jl:100[24m[39m
[33m[1m│ [22m[39m [14] [0m[1mwithpath[22m[0m[1m([22m[90mf[39m::[0mAny, [90mpath[39m::[0mString[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[36mRequires[39m [90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mrequire.jl:37[24m[39m
[33m[1m│ [22m[39m [15] [0m[1m(::LinearSolve.var"#86#95")[22m[0m[1m([22m[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[35mLinearSolve[39m [90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mrequire.jl:99[24m[39m
[33m[1m│ [22m[39m [16] [0m[1m#invokelatest#2[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4messentials.jl:729[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [17] [0m[1minvokelatest[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4messentials.jl:726[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [18] [0m[1mforeach[22m[0m[1m([22m[90mf[39m::[0mtypeof(Base.invokelatest), [90mitr[39m::[0mVector[90m{Function}[39m[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mabstractarray.jl:2774[24m[39m
[33m[1m│ [22m[39m [19] [0m[1mloadpkg[22m[0m[1m([22m[90mpkg[39m::[0mBase.PkgId[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[36mRequires[39m [90mC:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\[39m[90m[4mrequire.jl:27[24m[39m
[33m[1m│ [22m[39m [20] [0m[1m#invokelatest#2[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4messentials.jl:729[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [21] [0m[1minvokelatest[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4messentials.jl:726[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [22] [0m[1mrun_package_callbacks[22m[0m[1m([22m[90mmodkey[39m::[0mBase.PkgId[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:869[24m[39m
[33m[1m│ [22m[39m [23] [0m[1m_tryrequire_from_serialized[22m[0m[1m([22m[90mmodkey[39m::[0mBase.PkgId, [90mpath[39m::[0mString, [90msourcepath[39m::[0mString, [90mdepmods[39m::[0mVector[90m{Any}[39m[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:944[24m[39m
[33m[1m│ [22m[39m [24] [0m[1m_require_search_from_serialized[22m[0m[1m([22m[90mpkg[39m::[0mBase.PkgId, [90msourcepath[39m::[0mString, [90mbuild_id[39m::[0mUInt64[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:1028[24m[39m
[33m[1m│ [22m[39m [25] [0m[1m_require[22m[0m[1m([22m[90mpkg[39m::[0mBase.PkgId[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:1315[24m[39m
[33m[1m│ [22m[39m [26] [0m[1m_require_prelocked[22m[0m[1m([22m[90muuidkey[39m::[0mBase.PkgId[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:1200[24m[39m
[33m[1m│ [22m[39m [27] [0m[1mmacro expansion[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4mloading.jl:1180[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [28] [0m[1mmacro expansion[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4mlock.jl:223[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [29] [0m[1mrequire[22m[0m[1m([22m[90minto[39m::[0mModule, [90mmod[39m::[0mSymbol[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:1144[24m[39m
[33m[1m│ [22m[39m [30] [0m[1meval[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4mboot.jl:368[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [31] [0m[1minclude_string[22m[0m[1m([22m[90mmapexpr[39m::[0mtypeof(REPL.softscope), [90mmod[39m::[0mModule, [90mcode[39m::[0mString, [90mfilename[39m::[0mString[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[90mBase[39m [90m.\[39m[90m[4mloading.jl:1428[24m[39m
[33m[1m│ [22m[39m [32] [0m[1msoftscope_include_string[22m[0m[1m([22m[90mm[39m::[0mModule, [90mcode[39m::[0mString, [90mfilename[39m::[0mString[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[32mSoftGlobalScope[39m [90mC:\Users\runneradmin\.julia\packages\SoftGlobalScope\u4UzH\src\[39m[90m[4mSoftGlobalScope.jl:65[24m[39m
[33m[1m│ [22m[39m [33] [0m[1mexecute_request[22m[0m[1m([22m[90msocket[39m::[0mZMQ.Socket, [90mmsg[39m::[0mIJulia.Msg[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[33mIJulia[39m [90mC:\Users\runneradmin\.julia\packages\IJulia\Vo51o\src\[39m[90m[4mexecute_request.jl:67[24m[39m
[33m[1m│ [22m[39m [34] [0m[1m#invokelatest#2[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4messentials.jl:729[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [35] [0m[1minvokelatest[22m
[33m[1m│ [22m[39m [90m @ [39m[90m.\[39m[90m[4messentials.jl:726[24m[39m[90m [inlined][39m
[33m[1m│ [22m[39m [36] [0m[1meventloop[22m[0m[1m([22m[90msocket[39m::[0mZMQ.Socket[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[33mIJulia[39m [90mC:\Users\runneradmin\.julia\packages\IJulia\Vo51o\src\[39m[90m[4meventloop.jl:8[24m[39m
[33m[1m│ [22m[39m [37] [0m[1m(::IJulia.var"#15#18")[22m[0m[1m([22m[0m[1m)[22m
[33m[1m│ [22m[39m [90m @ [39m[33mIJulia[39m [90m.\[39m[90m[4mtask.jl:484[24m[39m
[33m[1m│ [22m[39m in expression starting at C:\Users\runneradmin\.julia\packages\LinearSolve\qCLK7\ext\LinearSolveEnzymeExt.jl:1
[33m[1m└ [22m[39m[90m@ Requires C:\Users\runneradmin\.julia\packages\Requires\Z8rfN\src\require.jl:51[39m
After importing the packages, the path to the Functional Mock-up Units (FMUs) is set. The FMU is a model exported meeting the Functional Mock-up Interface (FMI) Standard. The FMI is a free standard (fmi-standard.org) that defines a container and an interface to exchange dynamic models using a combination of XML files, binaries and C code zipped into a single file.
The object-orientated structure of the SpringPendulum1D (simpleFMU) can be seen in the following graphic and corresponds to a simple modeling.
In contrast, the model SpringFrictionPendulum1D (realFMU) is somewhat more accurate, because it includes a friction component.
Next, the start time and end time of the simulation are set. Finally, a step size is specified to store the results of the simulation at these time steps.
tStart = 0.0
tStep = 0.01
tStop = 5.0
tSave = collect(tStart:tStep:tStop)
501-element Vector{Float64}:
0.0
0.01
0.02
0.03
0.04
0.05
0.06
0.07
0.08
0.09
0.1
0.11
0.12
⋮
4.89
4.9
4.91
4.92
4.93
4.94
4.95
4.96
4.97
4.98
4.99
5.0
RealFMU
In the next lines of code the FMU of the realFMU model from FMIZoo.jl is loaded and the information about the FMU is shown.
realFMU = fmiLoad("SpringFrictionPendulum1D", "Dymola", "2022x")
fmiInfo(realFMU)
#################### Begin information for FMU ####################
Model name: SpringFrictionPendulum1D
FMI-Version: 2.0
GUID: {2e178ad3-5e9b-48ec-a7b2-baa5669efc0c}
Generation tool: Dymola Version 2022x (64-bit), 2021-10-08
Generation time: 2022-05-19T06:54:12Z
Var. naming conv.: structured
Event indicators: 24
Inputs: 0
Outputs: 0
States: 2
33554432 ["mass.s"]
33554433 ["mass.v", "mass.v_relfric"]
Supports Co-Simulation: true
Model identifier: SpringFrictionPendulum1D
Get/Set State: true
Serialize State: true
Dir. Derivatives: true
Var. com. steps: true
Input interpol.: true
Max order out. der.: 1
Supports Model-Exchange: true
Model identifier: SpringFrictionPendulum1D
Get/Set State: true
Serialize State: true
Dir. Derivatives: true
##################### End information for FMU #####################
In the next steps the parameters are defined. The first parameter is the initial position of the mass, which is initilized with $0.5𝑚$. The second parameter is the initial velocity of the mass, which is initialized with $0\frac{m}{s}$. The FMU hase two states: The first state is the position of the mass and the second state is the velocity. In the function fmiSimulate() the realFMU is simulated, still specifying the start and end time, the parameters and which variables are recorded. After the simulation is finished the result of the realFMU can be plotted. This plot also serves as a reference for the other model (simpleFMU).
initStates = ["s0", "v0"]
x₀ = [0.5, 0.0]
params = Dict(zip(initStates, x₀))
vrs = ["mass.s", "mass.v", "mass.a", "mass.f"]
realSimData = fmiSimulate(realFMU, (tStart, tStop); parameters=params, recordValues=vrs, saveat=tSave)
fmiPlot(realSimData)
[34mSimulating CS-FMU ... 0%|█ | ETA: N/A[39m
[34mSimulating CS-FMU ... 100%|██████████████████████████████| Time: 0:00:02[39m
The data from the simulation of the realFMU, are divided into position and velocity data. These data will be needed later.
velReal = fmi2GetSolutionValue(realSimData, "mass.v")
posReal = fmi2GetSolutionValue(realSimData, "mass.s")
501-element Vector{Float64}:
0.5
0.5002235448486548
0.5008715291319449
0.5019478597521578
0.5034570452098334
0.5053993458877354
0.5077764240578201
0.5105886522837868
0.5138351439717114
0.5175150321322992
0.521627087567517
0.5261682148972211
0.5311370185654775
⋮
1.0657564963384756
1.066930862706352
1.0679715872270086
1.068876303469867
1.0696434085045978
1.0702725656148622
1.0707609890298837
1.071107075846018
1.0713093338869186
1.0713672546639146
1.0713672546629138
1.071367254661913
After extracting the data, the FMU is cleaned-up.
fmiUnload(realFMU)
SimpleFMU
The following lines load, simulate and plot the simpleFMU just like the realFMU. The differences between both systems can be clearly seen from the plots. In the plot for the realFMU it can be seen that the oscillation continues to decrease due to the effect of the friction. If you simulate long enough, the oscillation would come to a standstill in a certain time. The oscillation in the simpleFMU behaves differently, since the friction was not taken into account here. The oscillation in this model would continue to infinity with the same oscillation amplitude. From this observation the desire of an improvement of this model arises.
simpleFMU = fmiLoad("SpringPendulum1D", "Dymola", "2022x")
fmiInfo(simpleFMU)
vrs = ["mass.s", "mass.v", "mass.a"]
simpleSimData = fmiSimulate(simpleFMU, (tStart, tStop); recordValues=vrs, saveat=tSave, reset=false)
fmiPlot(simpleSimData)
#################### Begin information for FMU ####################
Model name: SpringPendulum1D
FMI-Version: 2.0
GUID: {fc15d8c4-758b-48e6-b00e-5bf47b8b14e5}
Generation tool: Dymola Version 2022x (64-bit), 2021-10-08
Generation time: 2022-05-19T06:54:23Z
Var. naming conv.: structured
Event indicators: 0
Inputs: 0
Outputs: 0
States: 2
33554432 ["mass.s"]
33554433 ["mass.v"]
Supports Co-Simulation: true
Model identifier: SpringPendulum1D
Get/Set State: true
Serialize State: true
Dir. Derivatives: true
Var. com. steps: true
Input interpol.: true
Max order out. der.: 1
Supports Model-Exchange: true
Model identifier: SpringPendulum1D
Get/Set State: true
Serialize State: true
Dir. Derivatives: true
##################### End information for FMU #####################
The data from the simulation of the simpleFMU, are divided into position and velocity data. These data will be needed later to plot the results.
velSimple = fmi2GetSolutionValue(simpleSimData, "mass.v")
posSimple = fmi2GetSolutionValue(simpleSimData, "mass.s")
501-element Vector{Float64}:
0.5
0.5003127019074967
0.5012175433745238
0.5027172504687035
0.504812416566759
0.5075012719497328
0.5107830165354977
0.5146534880772458
0.5191107030735219
0.5241484264969329
0.5297629811612266
0.5359472314461261
0.5426950964528339
⋮
1.6842615646003007
1.6884869953422783
1.6921224800662573
1.69516502108285
1.6976144547672483
1.6994659284032172
1.7007174453690572
1.7013675684067706
1.7014154196220592
1.7008606804843265
1.69970552855305
1.6979508813706
NeuralFMU
Loss function
In order to train our model, a loss function must be implemented. The solver of the NeuralFMU can calculate the gradient of the loss function. The gradient descent is needed to adjust the weights in the neural network so that the sum of the error is reduced and the model becomes more accurate.
The loss function in this implementation consists of the mean squared error (mse) from the real position of the realFMU simulation (posReal) and the position data of the network (posNet). $ e{mse} = \frac{1}{n} \sum\limits{i=0}^n (posReal[i] - posNet[i])^2 $
As it is indicated with the comments, one could also additionally consider the mse from the real velocity (velReal) and the velocity from the network (velNet). The error in this case would be calculated from the sum of both errors.
# loss function for training
function lossSum(p)
global posReal
solution = neuralFMU(x₀; p=p)
posNet = fmi2GetSolutionState(solution, 1; isIndex=true)
FMIFlux.Losses.mse(posReal, posNet)
end
lossSum (generic function with 1 method)
Callback
To output the loss in certain time intervals, a callback is implemented as a function in the following. Here a counter is incremented, every twentieth pass the loss function is called and the average error is printed out.
# callback function for training
global counter = 0
function callb(p)
global counter += 1
if counter % 20 == 1
avgLoss = lossSum(p[1])
@info "Loss [$counter]: $(round(avgLoss, digits=5)) Avg displacement in data: $(round(sqrt(avgLoss), digits=5))"
end
end
callb (generic function with 1 method)
Structure of the NeuralFMU
In the following, the topology of the NeuralFMU is constructed. It consists of an input layer, which then leads into the simpleFMU model. The ME-FMU computes the state derivatives for a given system state. Following the simpleFMU is a dense layer that has exactly as many inputs as the model has states (and therefore state derivatives). The output of this layer consists of 16 output nodes and a tanh activation function. The next layer has 16 input and output nodes with the same activation function. The last layer is again a dense layer with 16 input nodes and the number of states as outputs. Here, it is important that no tanh-activation function follows, because otherwise the pendulums state values would be limited to the interval $[-1;1]$.
# NeuralFMU setup
numStates = fmiGetNumberOfStates(simpleFMU)
net = Chain(x -> simpleFMU(x=x, dx_refs=:all),
Dense(numStates, 16, tanh),
Dense(16, 16, tanh),
Dense(16, numStates))
Chain(
var"#1#2"(),
Dense(2 => 16, tanh), [90m# 48 parameters[39m
Dense(16 => 16, tanh), [90m# 272 parameters[39m
Dense(16 => 2), [90m# 34 parameters[39m
) [90m # Total: 6 arrays, [39m354 parameters, 1.758 KiB.
Definition of the NeuralFMU
The instantiation of the ME-NeuralFMU is done as a one-liner. The FMU (simpleFMU), the structure of the network net
, start tStart
and end time tStop
, the numerical solver Tsit5()
and the time steps tSave
for saving are specified.
neuralFMU = ME_NeuralFMU(simpleFMU, net, (tStart, tStop), Tsit5(); saveat=tSave);
Plot before training
Here the state trajectory of the simpleFMU is recorded. Doesn't really look like a pendulum yet, but the system is random initialized by default. In the plots later on, the effect of learning can be seen.
solutionBefore = neuralFMU(x₀)
fmiPlot(solutionBefore)
[33m[1m┌ [22m[39m[33m[1mWarning: [22m[39mNo solver keyword detected for NeuralFMU.
[33m[1m│ [22m[39mContinuous adjoint method is applied, which requires solving backward in time.
[33m[1m│ [22m[39mThis might be not supported by every FMU.
[33m[1m│ [22m[39m(This message is only printed once.)
[33m[1m└ [22m[39m[90m@ FMICore C:\Users\runneradmin\.julia\packages\FMICore\7NIyu\src\printing.jl:38[39m
Training of the NeuralFMU
For the training of the NeuralFMU the parameters are extracted. The known Adam optimizer for minimizing the gradient descent is used as further passing parameters. In addition, the previously defined loss and callback function, as well as the number of epochs are passed.
# train
paramsNet = FMIFlux.params(neuralFMU)
optim = Adam()
FMIFlux.train!(lossSum, neuralFMU, Iterators.repeated((), 300), optim; cb=()->callb(paramsNet))
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1]: 14.31508 Avg displacement in data: 3.78353
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [21]: 2.0444 Avg displacement in data: 1.42982
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [41]: 0.36163 Avg displacement in data: 0.60135
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [61]: 0.11469 Avg displacement in data: 0.33866
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [81]: 0.0737 Avg displacement in data: 0.27147
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [101]: 0.06571 Avg displacement in data: 0.25633
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [121]: 0.06033 Avg displacement in data: 0.24562
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [141]: 0.05599 Avg displacement in data: 0.23663
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [161]: 0.05242 Avg displacement in data: 0.22894
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [181]: 0.0495 Avg displacement in data: 0.22249
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [201]: 0.04714 Avg displacement in data: 0.21712
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [221]: 0.04522 Avg displacement in data: 0.21265
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [241]: 0.04366 Avg displacement in data: 0.20896
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [261]: 0.04239 Avg displacement in data: 0.20589
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [281]: 0.04135 Avg displacement in data: 0.20334
Comparison of the plots
Here three plots are compared with each other and only the position of the mass is considered. The first plot represents the simpleFMU, the second represents the realFMU (reference) and the third plot represents the result after training the NeuralFMU.
# plot results mass.s
solutionAfter = neuralFMU(x₀)
fig = Plots.plot(xlabel="t [s]", ylabel="mass position [m]", linewidth=2,
xtickfontsize=12, ytickfontsize=12,
xguidefontsize=12, yguidefontsize=12,
legendfontsize=8, legend=:topright)
Plots.plot!(fig, tSave, posSimple, label="SimpleFMU", linewidth=2)
Plots.plot!(fig, tSave, posReal, label="RealFMU", linewidth=2)
Plots.plot!(fig, solutionAfter; stateIndices=1:1, values=false, label="NeuralFMU (300 epochs)", linewidth=2)
fig
Continue training and plotting
As can be seen from the previous figure, the plot of the NeuralFMU has not yet fully converged against the realFMU, so the training of the NeuralFMU is continued. After further training, the plot of NeuralFMU is added to the figure again. The effect of the longer training can be recognized well, since the plot of the NeuralFMU had further converged.
FMIFlux.train!(lossSum, neuralFMU, Iterators.repeated((), 1200), optim; cb=()->callb(paramsNet))
# plot results mass.s
solutionAfter = neuralFMU(x₀)
Plots.plot!(fig, solutionAfter; stateIndices=1:1, values=false, label="NeuralFMU (1500 epochs)", linewidth=2)
fig
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [301]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [321]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [341]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [361]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [381]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [401]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [421]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [441]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [461]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [481]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [501]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [521]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [541]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [561]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [581]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [601]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [621]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [641]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [661]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [681]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [701]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [721]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [741]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [761]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [781]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [801]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [821]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [841]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [861]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [881]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [901]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [921]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [941]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [961]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [981]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1001]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1021]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1041]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1061]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1081]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1101]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1121]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1141]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1161]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1181]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1201]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1221]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1241]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1261]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1281]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1301]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1321]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1341]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1361]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1381]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1401]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1421]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1441]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1461]: 0.04052 Avg displacement in data: 0.20129
[36m[1m[ [22m[39m[36m[1mInfo: [22m[39mLoss [1481]: 0.04052 Avg displacement in data: 0.20129
Finally, the FMU is cleaned-up.
fmiUnload(simpleFMU)
Summary
Based on the plots, it can be seen that the NeuralFMU is able to adapt the friction model of the realFMU. After 300 runs, the curves do not overlap very well, but this can be achieved by longer training (1000 runs) or a better initialization.
Source
[1] Tobias Thummerer, Lars Mikelsons and Josef Kircher. 2021. NeuralFMU: towards structural integration of FMUs into neural networks. Martin Sjölund, Lena Buffoni, Adrian Pop and Lennart Ochel (Ed.). Proceedings of 14th Modelica Conference 2021, Linköping, Sweden, September 20-24, 2021. Linköping University Electronic Press, Linköping (Linköping Electronic Conference Proceedings ; 181), 297-306. DOI: 10.3384/ecp21181297