Importing FMUs
FMUs, or Functional Mock-up Units, are a standard for passing binary representations of models that any simulation tool can use.
Dyad offers support to load FMUs, and there are experimental efforts to save Dyad models to FMUs via the juliac mechanism. For now, we will focus on loading and using FMUs. Dyad supports using FMUs via the external component mechanism.
Loading FMUs
At the moment, you need to manually define the interface of the FMU, that being the set of variables and parameters that the FMU exposes.
Manual definition of the Dyad interface for the FMU is a current limitation of the system which will be lifted in future releases.
Thus, in Dyad, you define a partial component that has the "API" of the FMU, and then an external component that extend
s that partial component, whose implementation will live in Julia.
partial component PartialMyFMUComponent
parameter a
parameter b
variable x
variable y
# so on and so forth...
end
component MyFMUComponent
extends PartialMyFMUComponent
end
Loading the FMU itself is done via the (manually coded) Julia implementation of the FMU. You can load the FMU either in model exchange or co-simulation mode. This is done using the ModelingToolkit FMU import interface.
using ModelingToolkit
using FMI # to load FMUs
function MyFMUComponent(; name, params...)
fmu = FMI.loadFMU(
"path/to/your.fmu",
type = :ME # :ME => model exchange, :CS => co-simulation
)::FMI.FMI2 # change this to FMI.FMI3 for FMI v3 standard
component = ModelingToolkit.FMIComponent(
Val(2); # FMI v2 standard, Val(3) for FMI v3 standard
fmu, # the FMU object
name, # the name of the component, from kwargs
type = :ME, # same as at load time
)
# Set default values for components
default_keys = keys(ModelingToolkit.get_defaults(component))
for (param, value) in params
param in default_keys || error("
Parameter $param is not a valid parameter for this FMU $name.")
param_symbol = ModelingToolkit.UnPack.unpack(component, Val(param))
ModelingToolkit.get_defaults(component)[param_symbol] = value
end
# Finally, return the adjusted component
return component
end
Working example
Here's a working example of a Dyad model that loads an FMU and uses it as an external component.
We will load an adder FMU, corresponding to this Dyad model:
# This implements the behaviour of the adder in Dyad
component DyadAdder
variable a::Real
variable b::Real
variable c::Real
out = RealOutput()
out2 = RealOutput()
parameter value::Real = 1.0
relations
initial a = 1
initial b = 1
initial c = 1
out = a+b+value
D(c) = out
out2 = 2c
end
Let's now define an external component that we will load the FMU into:
# This is a "hack" to get the adder API - which is the parameters and connectors
# but have the adder itself be defined externally.
external component FMUAdder
a = RealInput()
b = RealInput()
c = RealInput()
out = RealOutput()
out2 = RealOutput()
parameter value::Real = 1.0
end
Now, we define the Julia loading function, in some file in the src/
directory of our Dyad project.
using ModelingToolkit, FMI
function FMUAdder(; name, params...)
fmu = FMI.loadFMU(
joinpath(dirname(dirname(Base.pathof(ModelingToolkit))), "test", "fmi", "fmus", "SimpleAdder.fmu"), # get a test FMU from ModelingToolkit
type = :ME # :ME => model exchange, :CS => co-simulation
)::FMI.FMU2 # change this to FMI.FMU3 for FMI v3 standard
component = ModelingToolkit.FMIComponent(
Val(2); # FMI v2 standard, Val(3) for FMI v3 standard
fmu, # the FMU object
name, # the name of the component, from kwargs
type = :ME, # same as at load time
)
# Set default values for components
default_keys = keys(ModelingToolkit.get_defaults(component))
for (param, value) in params
param in default_keys || error("
Parameter $param is not a valid parameter for this FMU $name.")
param_symbol = ModelingToolkit.UnPack.unpack(component, Val(param))
ModelingToolkit.get_defaults(component)[param_symbol] = value
end
# Finally, return the adjusted component
return component
end
This is what that FMU component looks like when constructed:
@named adder = FMUAdder()
This adder is incomplete as is. Let's create another Dyad component that uses the FMU:
# This component links two adders tgether.
partial component AbstractAdderLinker
adder1 = DyadAdder(value=2)
adder2 = DyadAdder(value=2)
variable x::Real
relations
initial adder1.c = 1
initial adder2.c = 1
initial adder1.a = 2
connect(adder1.a, adder2.out2)
connect(adder2.a, adder1.out2)
adder1.b = 1
adder2.b = 2
der(x) = x
end
Now, we can create a simple model that only uses Dyad, and one that uses the FMU:
component DyadDyadLinked
extends AbstractAdderLinker(
adder1 = DyadAdder(value=2),
adder2 = DyadAdder(value=2)
)
end
component DyadFMULinked
extends AbstractAdderLinker(
adder1 = DyadAdder(value=2),
adder2 = FMUAdder(value=2)
)
end
We can of course simulate these models. Bear in mind that the FMU components do not natively support automatic differentiation, since they are opaque binaries, so we will have to use a specific solver with automatic differentiation set to be off.
First, let's build the components:
using Main.var"##build/.dyad/manual/advanced/fmuDyadHygiene#429".fmu # hide
using ModelingToolkit,
OrdinaryDiffEq
@mtkbuild dsys = DyadDyadLinked()
@mtkbuild fsys = DyadFMULinked()
and then we can simulate and plot:
using Main.var"##build/.dyad/manual/advanced/fmuDyadHygiene#429".fmu # hide
dsol = solve(ODEProblem(dsys, [], (0, 1)), Rodas5P(; autodiff = false))
fsol = solve(ODEProblem(fsys, [], (0, 1)), Rodas5P(; autodiff = false))
all(dsol ≈ fsol)
Let's plot the results:
using Main.var"##build/.dyad/manual/advanced/fmuDyadHygiene#429".fmu # hide
using CairoMakie
f, a1, p1 = plot(dsol; axis = (; title = "Dyad only"), figure = (; size = (800, 400)))
a2, p2 = plot(f[1, 2], fsol; axis = (; title = "Dyad + FMU"))
axislegend(a1; position = :lt); axislegend(a2; position = :lt)
f