Deploying a Lotka-Volterra FMU model

In this example we will demonstrate how to deploy a Lotka-Volterra model as a FMU using FMUGeneration.jl. We start by loading FMUGeneration and OrdinaryDiffEq into our environment.

using FMUGeneration
using OrdinaryDiffEq

For the tutorial, we chose a Lotka Volterra system with controls. This system is a pair of non-linear differential equations used to describe the dynamics of a biological system. The system has a set of parameters α, β, γ, δ and u₁ , u₂ are time-dependent controls.

\[\frac{dx₁}{dt} = \alpha x₁ - \beta x₁x₂ + u₁ \]

\[\frac{dx₂}{dt} = \delta x₁x₂ - \gamma x₂ - u₂\]

FMUGeneration.jl first creates a package for the FMU containing the necessary functions, models, and variables, then builds a sysimage.

We define the above system as an expression to be packaged into an FMU as follows:

lv_expr = :(function (dx, x, u, p, t)
    x₁, x₂ = x
    u₁, u₂ = u
    α, β, γ, δ = p
    dx₁ = α * x₁ - β * x₁ * x₂ + u₁
    dx₂ = δ * x₁ * x₂ - γ * x₂ - u₂
    dx = [dx₁, dx₂]
end);
:(function (dx, x, u, p, t)
      #= lv.md:24 =#
      #= lv.md:25 =#
      (x₁, x₂) = x
      #= lv.md:26 =#
      (u₁, u₂) = u
      #= lv.md:27 =#
      (α, β, γ, δ) = p
      #= lv.md:28 =#
      dx₁ = (α * x₁ - β * x₁ * x₂) + u₁
      #= lv.md:29 =#
      dx₂ = (δ * x₁ * x₂ - γ * x₂) - u₂
      #= lv.md:30 =#
      dx = [dx₁, dx₂]
  end)

Once we have defined our system and continuous model, we need to define some defaults for the system. These include initial states, initial inputs, default parameters, and the timespan.

initial_states = [1.0, 1.0]
initial_inputs = [0.01, 0.01]
default_parameters = [2.0, 1.875, 2.0, 1.875]
tspan = (0.0, 10.0)
param_names = ["α", "β", "γ", "δ"]
input_names = ["u_1" , "u_2"]
state_names = ["x_1" , "x_2"]
2-element Vector{String}:
 "x_1"
 "x_2"

We then instantiate a JuliaFMU object which comines the meradata, dependencies and dynamics which make up the FMU. This implicitly generates the metadata.

fmu = JuliaFMU(
    # REQUIRED ARGUMENTS

    # We specify the FMU name
    "lotka-volterra",
    # We specify the FMI version
    FMI_V3, # or v2,
    # We specify the FMU operating types supported
    [FMI_MODELEXCHANGE, FMI_COSIMULATION];

    # OPTIONAL ARGUMENTS

    # We optionally specify the default time-space of the FMU operation
    default_tspan = tspan,
    # We optionally specify the recommended step size
    # default_stepsize = 1e-3,
    # We optionally specify the recommended solver tolerance
    # default_tolerance = 1e-6,


    # Metadata: inputs, parameters and states respectively
    inputs = [
        (name=input_names[i], start=initial_inputs[i]) for i in 1:length(input_names)
    ],
    parameters = [
        (name=param_names[i], start=default_parameters[i]) for i in 1:length(param_names)
    ],
    states = [
        (name=state_names[i], start=initial_states[i]) for i in 1:length(state_names)
    ],

    # We define the dependencies required for the FMU. Here, we need OrdinaryDiffEq for the solver used to run the FMU in CS mode.
    dependencies = @deps([OrdinaryDiffEq]),
    # We define the ODE function expression with the signature `(dx, u, p, t) -> begin ... end` where the function is expected to be inplace. Here, it is the `double_pendulum_expr` defined earlier. This is an essential kwarg for all ME FMUs.
    ode_function = lv_expr,
    # We optionally define function to compute the outputs with the signature `(x, u, p, t) -> outs`.
    # observables_function = observable_expr,
    # We specify which solver to use for cosimulation. We default to `OrdinaryDiffEq.AutoTsit5(OrdinaryDiffEq.FBDF())` if not provided.
    cosimulator_solver = :(OrdinaryDiffEq.AutoTsit5(OrdinaryDiffEq.FBDF())),
    # If we had inputs, we would also had to specify it like below:
    # inputs = [
    #   (name="input_1", start=1.0),
    #   ...
    # ],

    # If we had to initialize out states in a specific manner, we could also that like below:
    # state_initializer = :((x, u, p, t) -> initialize_x)

    # We could also optionally provide integrator options:
    # cosimulator_integrator_options=(abstol=1e-6, reltol=1e-6),

    # We could also optionally provide objects from user space needed for the FMU dynamics to operate
    # objects=@objects([test_obj]),

    # We optionally define the number of threads to start the FMU with to capitalize on multi-threaded acceleration of matmul operations. This is only relevant if your computation is heavy on matrix operations.
    # n_octavian_threads = 4
)
Functional Mockup Unit: lotka-volterra

FMI Specification: 
    Version:    v3
    Types:      ["modelExchange", "coSimulation"]

Default Experiment Setup:
    Time Span:  (0.0, 10.0)
    Step Size:  nothing
    Tolerance:  nothing

Paths:
    Temp:           /home/github_actions/actions-runner-1/home/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/wFHgQT
    Julia Package:  /home/github_actions/actions-runner-1/home/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/wFHgQT/FMIBinary_wFHgQT.jl
    JuliaFMU:       /home/github_actions/actions-runner-1/home/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/wFHgQT/lotka-volterra.fmu
    Metadata XML:   /home/github_actions/actions-runner-1/home/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/wFHgQT/modelDescription.xml