Functional Mockup Units tutorial
The JuliaSim software is available for preview only. Please contact us for access, by emailing info@juliacomputing.com, if you are interested in evaluating JuliaSim.
In this tutorial, we provide an overview of the common workflows that arise in the manipulation of Functional Mockup Units (FMUs). We first demonstrate how these files are loaded, and then present the two standard approaches to coupled simulation, namely, model exchange and co-simulation. We then present a method for generating FMUs. Finally, we show how FMUs can be easily converted to a system of ordinary differential equations (ODEs).
Loading FMUs
The FMUs are loaded by specifying the absolute path to the FMU file. The remaining positional argument required by the FMUSimSetup
constructor is keys
(or keynames
). This argument is specific to the FMU file. For instance, in the case of a heating FMU, the keys
consist of the heated unit temperature set point and the set point of distribution circuit temperature.
The keyword arguments include:
fmi_type
: Default:"ModelExchange"
. The type of coupled simulation.
Available options: "ModelExchange"
or "CoSimulation"
. See below for more details.
rel_tol
: Default: 1e-5. This keyword specifies the relative error tolerance.solver
: Default:Tsit5()
. The solver to be used in the simulation.outputs
: Default:String[]
.dt
: Default:nothing
. Describes the initial step size.
Below is a simple example of using the FMUSimSetup
constructor:
fmu_filename = joinpath(@__DIR__, "test_fmus", "heating.fmu")
keynames = ["Tu0", "Td0"]
setup = FMUs.FMUSimSetup(fmu_filename, keynames, solver = Rosenbrock23(autodiff=false))
Model exchange vs co-simulation
Model exchange refers to the composition of surrogatized Continuous-Time Echo State Networks (CTESNs) with models from external language (in this case, the Functional Mock-up Interface), while in co-simulation the FMUs export their own simulation routine, which is then synchronized by means of a master algorithm.
Model exchange
We first illustrate how to perform model exchange. We will rely on surrogatization in this example. Suppose we have an FMU called "Temperature"
. Note that this model exchange example requires, amongst others, the JuliaSimSurrogates.jl
package. As described above, we set up the constructor as follows:
using FMUs, JuliaSimSurrogates, OrdinaryDiffEq
fmu_filename = joinpath(@__DIR__, "test_fmus", "Temperature.fmu")
keynames = ["JLoad", "TLoad", "Va", "Ve"]
test = [0.15, 63.66, 100.0, 100.0]
param_space = JuliaSimSurrogates.gen_paramspace(test)
ts = 0:0.1:3.0 # timestamps
setup = FMUSimSetup(fmu_filename, keynames, solver = ImplicitEuler(autodiff=false))
Recall from the introductory sections that we don't have to specify the "ModelExchange"
option in the constructor because it is the default value in FMUSimSetup
.
We now use the JuliaSimSurrogates.jl
package to create the surrogatized CTESN. For more information on generating surrogates, consult this tutorial.
# Linear Continuous-Time Echo State Network (LPCTESN)
surralg = LPCTESN(1000)
truth = JuliaSimSurrogates.simulate(setup, test; ts = ts)
surr = JuliaSimSurrogates.surrogatize(
setup,
param_space,
surralg,
100; # n_sample_pts
component_name = :DCPM_Temperature,
hybrid_sim_kwargs = (;ts=ts),
ensemble_kwargs = (;ts=ts),
verbose=true
)
Co-Simulation
We now focus on how to set up a co-simulation problem. See also the subsection below for a method of converting FMUs to an ODESystem
.
Let's assume there is an FMU describing a V6 engine. As before, we specify the absolute path to the file, add a relevant key, and select the "CoSimulation"
option in the constructor:
using FMUs
using FMUs.FMI
fmu_filename = joinpath(@__DIR__, "test_fmus", "EngineV6.fmu")
keynames = ["load.J"]
setup = FMUSimSetup(fmupath, keynames, fmi_type = "CoSimulation",
dt = 0.01, outputs = ["filteredEngineTorque"])
Finally, we specify the test range and timestamps. We then use all the information to invoke the simulate
function:
test = [1.0]
ts = 0:1e-2:1
sim_result = FMUs.simulate(setup, test, ts = ts)
Generating FMUs
FMUs can be generated from a model specified in Julia. The input for the generator is an ODESystem
from ModelingToolkit
or an ODESystem
with its surrogate, which is then parsed into an XML file. The surrogate generates a surrogatized ODESystem
for the given parameter set p
. Note that setting custom parameters (from the FMU simulator's end) automatically updates the surrogatized ODESystem
. Finally, C or C-callable code is generated to reflect whether model exchange or co-simulation is required (see also the discussion above). The output produced by the generator has the .fmu
extension. For more information on the FMI standard, consult this source.
The API for FMU generation is as follows:
generateFMU(surrogate, ode_system, initial_condition, simulation_parameters, initial_time, model_type, output_path; generateXML, generateLibs, verbose)
Note:
model_type
is aSymbol
, either:ML
(model exchange) or:CS
(co-simulation)generateXML
defaults totrue
generateLibs
defaults totrue
verbose
defaults totrue
We will now discuss the generation process in greater detail. As an example, we will take Robertson's chemical reaction model (known for its stiffness). For more information on simulation and surrogatization, consult this document. Observe that this example takes an ODESystem
with its surrogate. As noted above, the :ME
option in the generateFMU
function specifies that model exchange is required.
using FMUGeneration, ModelingToolkit, OrdinaryDiffEq, JuliaSimSurrogates
function rober(du, u, p, t)
y₁, y₂, y₃ = u # initial vector
k₁, k₂, k₃ = p # rate constants
du[1] = -k₁ * y₁ + k₃ * y₂ * y₃
du[2] = k₁ * y₁ - k₂ * y₂^2 - k₃ * y₂ * y₃
du[3] = k₂ * y₂^2
nothing
end
tstop = 1e4
p = [0.04, 3e7, 1e4] # simulation parameters
u0 = [1.0, 0.0, 0.0] # initial condition
tspan = (0.0, tstop)
prob = ODEProblem(rober, u0, tspan, p) # set up the ODE problem
# specify the parameter space
param_space = [(0.036, 0.044), (2.7e7, 3.3e7), (0.9e4, 1.1e4)]
# Linear Continuous-Time Echo State Network (LPCTESN)
surralg = LPCTESN(1000, output = 1:3)
# simulator for differential equations
sim = DEProblemSimulation(prob, reltol = 1e-12, abstol = 1e-12)
# construct the ODE surrogate
odesurrogate = JuliaSimSurrogates.surrogatize(
sim,
param_space,
surralg,
100; # n_sample_pts
ensemble_kwargs = (;),
component_name = :robertson_surrogate,
verbose=true)
# set up the ODE system with ModelingToolkit
sys = ODESystem(odesurrogate, p; name=Symbol("robertson_surrogate"))
generateFMU(odesurrogate, sys, [u0;odesurrogate.r0;odesurrogate.hybrid_solution.hybrid_solution_original[1]], p, tspan[1], :ME, "output_path")
Converting FMUs to an ODESystem
This procedure relies on the ModelingToolkit.jl
and OrdinaryDiffEq.jl
packages. We first specify the absolute path to the FMU and complete the FMUSimSetup
constructor.
using FMUs
using ModelingToolkit
using OrdinaryDiffEq
fmupath = joinpath(@__DIR__, "test_fmus", "EngineV6.fmu")
dt = 1e-2
setup = FMUSimSetup(fmupath, fmi_type = CoSimulation, dt = dt)
tspan = (0.0, 1.0)
Once the fmi
component has been specified, we can use ModelingToolkit.jl
to convert the FMU to an ODESystem
, and then solve it via OrdinaryDiffEq.jl
:
@named sys = ODESystem(setup, tspan)
prob = ODEProblem(sys, [], (0.0, 1.0+eps(1.0))) # so that we include the last step
sol = solve(prob, Euler(); tstops=setup.dt, callback=prob.kwargs[:difference_cb], adaptive=false)
ts = 0:dt:1
r = FMUs.simulate(setup, ts) # simulate the FMU via ModelingToolkit