The JuliaSim software is available for preview only. Please contact us for access, by emailing firstname.lastname@example.org, 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).
The FMUs are loaded by specifying the absolute path to the FMU file. The remaining positional argument required by the
FMUSimSetup constructor is
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:
"ModelExchange". The type of coupled simulation.
"CoSimulation". See below for more details.
rel_tol: Default: 1e-5. This keyword specifies the relative error tolerance.
Tsit5(). The solver to be used in the simulation.
nothing. Describes the initial step size.
Below is a simple example of using the
fmu_filename = joinpath(@__DIR__, "test_fmus", "heating.fmu") keynames = ["Tu0", "Td0"] setup = FMUs.FMUSimSetup(fmu_filename, keynames, solver = Rosenbrock23(autodiff=false))
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.
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
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 )
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
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
test = [1.0] ts = 0:1e-2:1 sim_result = FMUs.simulate(setup, test, ts = ts)
FMUs can be generated from a model specified in Julia. The input for the generator is an
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)
model_type is a
:ML (model exchange) or
generateLibs defaults to
verbose defaults to
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 = -k₁ * y₁ + k₃ * y₂ * y₃ du = k₁ * y₁ - k₂ * y₂^2 - k₃ * y₂ * y₃ du = 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], p, tspan, :ME, "output_path")
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)
fmi component has been specified, we can use
ModelingToolkit.jl to convert the FMU to an
ODESystem, and then solve it via
@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