Deploying a Double Pendulum Model with Output Transform

This tutorial will demonstrate how to deploy a double pendulum model with a transformation of the outputs. The system evolves with states as polar coordinates of the position, but we will add a transformation such that the FMU outputs are Cartesian coordinates.

We start with importing FMUGeneration and OrdinaryDiffEq into our environment.

using FMUGeneration
using OrdinaryDiffEq

We first define the system and the transforming functions' expressions. The function double_pendulum is our system. The function polar2cart is the transforming function that converts the polar coordinates into cartesian. This example is taken from the DifferentialEquations.jl docs.

double_pendulum_exp = :(function (dx, x, u, p, t)
    m₁, m₂, L₁, L₂, g = p
    dx[1] = x[2]
    dx[2] = -((g * (2 * m₁ + m₂) * sin(x[1]) +
                    m₂ * (g * sin(x[1] - 2 * x[3]) +
                    2 * (L₂ * x[4]^2 + L₁ * x[2]^2 * cos(x[1] - x[3])) * sin(x[1] - x[3]))) /
                (2 * L₁ * (m₁ + m₂ - m₂ * cos(x[1] - x[3])^2)))
    dx[3] = x[4]
    dx[4] = (((m₁ + m₂) * (L₁ * x[2]^2 + g * cos(x[1])) +
                L₂ * m₂ * x[4]^2 * cos(x[1] - x[3])) * sin(x[1] - x[3])) /
                (L₂ * (m₁ + m₂ - m₂ * cos(x[1] - x[3])^2))

end)

polar2cart_expr = :(function (x, u, p, t)
    l1 = p[3]
    l2 = p[4]
    vars = (2, 4)
    p1 = l1 * x[vars[1]]
    p2 = l2 * x[vars[2]]
    x1 = l1 * sin(p1)
    y1 = l1 * -cos(p1)
    [x1 + l2 * sin.(p2), y1 - l2 * cos.(p2)]
end);
:(function (x, u, p, t)
      #= double_pendulum.md:30 =#
      #= double_pendulum.md:31 =#
      l1 = p[3]
      #= double_pendulum.md:32 =#
      l2 = p[4]
      #= double_pendulum.md:33 =#
      vars = (2, 4)
      #= double_pendulum.md:34 =#
      p1 = l1 * x[vars[1]]
      #= double_pendulum.md:35 =#
      p2 = l2 * x[vars[2]]
      #= double_pendulum.md:36 =#
      x1 = l1 * sin(p1)
      #= double_pendulum.md:37 =#
      y1 = l1 * -(cos(p1))
      #= double_pendulum.md:38 =#
      [x1 + l2 * sin.(p2), y1 - l2 * cos.(p2)]
  end)

Now that we have defined our system and transformation into the FMU Package, let us define the initial states, default parameters, and timespan for the system.

initial_states = [0, π / 3, 0, 3pi / 5]
m₁, m₂, L₁, L₂, g = 1, 2, 1, 2, 9.8
default_parameters = [m₁, m₂, L₁, L₂, g]
tspan = (0.0, 50.0)
tend = tspan[end]

param_names = ["m1", "m2", "L1", "L2", "g"]
state_names = ["alpha", "Lalpha", "beta", "Lbeta"]
output_names = ["x", "y"]
2-element Vector{String}:
 "x"
 "y"

We instantiate a JuliaFMU

fmu = JuliaFMU(
    # REQUIRED ARGUMENTS

    # We specify the FMU name
    "double_pendulum",
    # 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: parameters, states and outputs respectively
    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)
    ],
    outputs = [
        (name=output_names[i], ) for i in 1:length(output_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 = double_pendulum_exp,
    # We define function to compute the outputs with the signature `(x, u, p, t) -> outs`. Here, it is the `polar2cart_expr` defined earlier.
    observables_function = polar2cart_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]),
)
Functional Mockup Unit: double_pendulum

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

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

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