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 a Symbol, either :ML (model exchange) or :CS (co-simulation)

  • generateXMLdefaults to true

  • generateLibs defaults to true

  • verbose defaults to true

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