Skip to content

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 extends that partial component, whose implementation will live in Julia.

dyad
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.

julia
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:

dyad
# 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:

dyad
# 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.

julia
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:

julia
@named adder = FMUAdder()
cˍt(t)=wrapper_1([c(t)],[a(t)b(t)],[value],t)out2(t)=wrapper_2([c(t)],[a(t)b(t)],[value],t)out(t)=wrapper_3([c(t)],[a(t)b(t)],[value],t)dc(t)dt=cˍt(t)

This adder is incomplete as is. Let's create another Dyad component that uses the FMU:

dyad

# 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:

dyad

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:

@example
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:

@example
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:

@example
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