Tutorials

Simulating an FMU

In this example, we will simulate an FMU of a ball bouncing around in a 2D space. The ball has a radius of 0.1 and the 2D space is given by by $[0.0, 1.0] \times [0.0, \infty[$

For this tutorial we will need OrdinaryDiffEq.jl to run the simulation and Plots.jl for visualisation.

using FMIExchange
using OrdinaryDiffEq
using Plots

To initialise the FMU, we must provide the path to the FMU, its inputs, outputs and states and the simulation start and stop times. If we wish to change the parameters of the FMU we can do this by providing a dictionary of parameter-value pairs.

bbloc = joinpath("deps", "fmu", "BouncingBall2D.fmu") # fmu file location
bbstart = 0.0 # simulation start
bbstop = 10.0 # simulation stop
bbins = String[] # FMU inputs (this FMU has none)
bbouts = String[] # FMU outputs (this FMU has none)
bbstates = ["dx", "dy", "x", "y"] # FMU states
bbparameters = Dict("eps"=>1e-2) # FMU parameters (optional)

We then instantiate the FMU as a CachedFMU2 (the name CachedFMU2 is to avoid naming conflicts with related packages). To simulate the FMU, we need to convert it to an AbstractSimModel. Simulation models can be directly simulated with OrdinaryDiffEq.jl. In this example we use the CachedModel simulation model, which contains preallocated caches to make calls to the FMU faster.

fmu = CachedFMU2(bbloc, bbstart, bbstop, bbins, bbouts, bbstates, bbparameters)
model = CachedModel(fmu)

Let's simulate the FMU with the default solver.

u0 = [1.0, 0.0, 0.5, 1.0]
p0 = Float64[]
tspan = (bbstart, bbstop)

sol = solve(
    ODEProblem(model, u0, tspan, p0),
    AutoTsit5(Rosenbrock23(autodiff=false)),
)
plot(sol, idxs=(3,4), legend=false)

That doesn't look right: the ball does not bounce when it hits the ground or the wall. There is a simple reason for this: we forgot to include callbacks to handle the FMU events. FMIExchange.jl can automatically generate the required callbacks. If we include them, the ball behaves as expected.

cbs = get_callbacks(model, bbstart, bbstop)
sol = solve(
    ODEProblem(model, u0, tspan, p0),
    AutoTsit5(Rosenbrock23(autodiff=false)),
    saveat=bbstart:0.01:bbstop, # for a nicer plot
    callback=CallbackSet(cbs...)
)
plot(sol, idxs=(3,4), legend=false)

Mixed Native ODEs and FMUs

This example will again simulate the bouncing ball FMU, but this time it adds a second ball that is defined by a native Julia ODE. We will call the balls bb and ss respectively. ss is subject to "screensaver physics": its speed is constant but ss changes direction when it collides with a wall or with bb. To avoid ss flying away, we restrict it to the $[0, 1] \times [0, 1]$ box.

First, define our bouncing ball FMU bb again (see Simulating an FMU).

using FMIExchange
using LinearAlgebra
using OrdinaryDiffEq
using Plots

bbloc = joinpath("deps", "fmu", "BouncingBall2D.fmu") # fmu file location
bbstart = 0.0 # simulation start
bbstop = 10.0 # simulation stop
bbins = String[] # FMU inputs (this FMU has none)
bbouts = String[] # FMU outputs (this FMU has none)
bbstates = ["dx", "dy", "x", "y"] # FMU states
bb_radius = 0.1
bbparameters = Dict("eps"=>1e-2, "r" => bb_radius) # FMU parameters (optional)

Now define the physics for ss.

ss_radius = 0.1
ymin = xmin = 0.0
ymax = xmax = 1.0
# The three statements below are a formality
ssinputs = ["dxs", "dys"]
ssoutputs = String[]
ssstates = ["xs", "ys"]

screensaver(du, u, p, t)  = du[:] = p[1:2]
screencb = VectorContinuousCallback(
    (out, u, t, integrator) -> begin
        out[1] = u[1] - (xmin + ss_radius)
        out[2] = u[1] - (xmax - ss_radius)
        out[3] = u[2] - (ymin + ss_radius)
        out[4] = u[2] - (ymax - ss_radius)
    end,
    (integrator, idx) -> begin
        if idx <= 2
            integrator.p[1] = -integrator.p[1]
        else
            integrator.p[2] = -integrator.p[2]
        end
    end,
    4
)

Now that we have both models, it's time to combine them. We wrap ss in a ModelSpecification and bb in an FMUSpecification which will allow us to use FMIExchange.jl's simulation composition functionality.

ss_spec = ModelSpecification(ssinputs, ssoutputs, ssstates, screensaver, (args...) -> nothing)
bb_spec = FMUSpecification(bbins, bbouts, bbstates, bbloc, bbparameters)
specs = [ss_spec, bb_spec]

The specifications can be converted to simulation models via the create_model function, which will automatically assign the correct address maps as well.

models = create_model(specs, start=bbstart, stop=bbstop)
iomap, umap = address_map(specs)

Since there are now two objects in our 2D space, we need to define a callback that handles collision between both.

collision_cb = ContinuousCallback(
    (u, t, integrator) -> begin
        (u[umap["x"]] - u[umap["xs"]]) ^ 2 + (u[umap["y"]] - u[umap["ys"]]) ^ 2 - (ss_radius + bb_radius) ^ 2
    end,
    (integrator) -> begin
        u, p = integrator.u, integrator.p

        # collision line and orthogonal
        collision_line = normalize([u[umap["x"]] - u[umap["xs"]], u[umap["y"]] - u[umap["ys"]]])
        orth = vec(nullspace(collision_line'))

        # velocity vectors of bb and ss
        vbb = @view(u[[umap["dx"], umap["dy"]]])
        vss = @view(p[[iomap["dxs"], iomap["dys"]]])

        # New vectors using dot(a, b) to project two vectors onto each other
        vbb[:] = abs(dot(collision_line, vbb)) * collision_line + dot(orth, vbb) * orth
        vss[:] = - abs(dot(collision_line, vss)) * collision_line + dot(orth, vss) * orth

        u_modified!(integrator, true)
    end
)
Collision Physics

To simulate the collision between ss and bb, we decompose the velocity vector of each into components that are orthogonal and tangential to the collision line (the line between the centers of ss and bb). The tangential component of bb's velocity vector will point away from ss after bouncing and vice versa. The orthogonal velocity component remains the same for both. For simplicity we assume the magnitude of each object's velocity remains constant.

Finally we define initial conditions and simulate the system. One small change compared to the first example, is that we now need to call the dynamics function to obtain our ODEfunction. This is because models is not an AbstractSimModel, but rather a Vector{AbstractSimModel}. dynamics(models) automatically generates an OrdinaryDiffEq.jl-compatible function that combines the ODEs of both models.

u0 = zeros(length(keys(umap)))
u0[umap["x"]] = 0.5
u0[umap["dx"]] = 0.5
u0[umap["y"]] = 1.0
u0[umap["dy"]] = 0.0
u0[umap["xs"]] = 0.2
u0[umap["ys"]] = 0.8
p0 = Float64[0.2, 0.2]
tspan = (bbstart, bbstop)
sol = solve(
    ODEProblem(dynamics(models), u0, tspan, p0),
    AutoTsit5(Rosenbrock23(autodiff=false)),
    callback=CallbackSet(
        reduce(vcat, get_callbacks.(models, bbstart, bbstop))...,
        screencb,
        collision_cb
    ),
    dtmax=0.01 # The collision callback may give errors when using large steps
)

We can plot the solution as a nice animation.

# this function was copied from
# https://discourse.julialang.org/t/plot-a-circle-with-a-given-radius-with-plots-jl/23295
function circleShape(h, k, r)
    θ = LinRange(0, 2π, 500)
    h .+ r * sin.(θ), k .+ r * cos.(θ)
end

anim = @animate for t = sort(bbstart:0.1:bbstop)
    plot(sol, idxs=[(umap["x"], umap["y"]), (umap["xs"], umap["ys"])],
         tspan=(bbstart, t), xlim=(xmin, xmax), ylim=(ymin, ymax),
         aspect_ratio=:equal, label=["bb" "ss"], legend=:topleft
    )
    plot!(circleShape(sol(t)[umap["x"]], sol(t)[umap["y"]], bb_radius),
          fill=(0,), color=palette(:default)[1], label=nothing)
    plot!(circleShape(sol(t)[umap["xs"]], sol(t)[umap["ys"]], ss_radius),
          fill=(0,), color=palette(:default)[2], label=nothing)
end