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