Dyad Builder, FMUs

I attended a webinar from Juliahub today on the topic of creating functional mockup units (FMUs) using Julia and Dyad.

Questions:

  • Is this feature available for academic users of Dyad Studio?
  • Is the graphical model editor, Dyad Builder, already released?

If I understand correctly, creating FMUs for co-simulation is supported by Dyad Studio, but not by the free library FMIExport .

Yes, it’s available to academic users of Dyad Studio. All you need to do is install the package FMUGeneration.jl (available on the Dyad Registry)

The graphical editor is not released yet. We will make an announcement when it is. If you would like to be an early user, feel free to email me or message me privately on Slack.

1 Like

Now trying the tutorial: Deploying a Lotka-Volterra FMU model · FMUGeneration

But I am getting the following error:


ERROR: The following 1 direct dependency failed to precompile:

FMIBinary_lO1AZO 

Failed to precompile FMIBinary_lO1AZO [15cc6e2c-9ed4-11f0-8ec7-8f1103c93337] to "/home/ufechner/.julia/compiled/v1.12/FMIBinary_lO1AZO/jl_dsoggK".
ERROR: LoadError: UndefVarError: `##meta#336` not defined in `FMIBinary_lO1AZO`
The binding may be too new: running in world age 38964, while current world is 38965.
Stacktrace:
  [1] #meta#1
    @ ~/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/c_api/ccallable.jl:11 [inlined]
  [2] meta
    @ ~/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/c_api/ccallable.jl:7 [inlined]
  [3] expand_ccallable(m::Module, rt::Any, def::Any)
    @ FMIBinary_lO1AZO.DeferredCCallable ~/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/c_api/ccallable.jl:47
  [4] var"@ccallable"(__source__::LineNumberNode, __module__::Module, def::Any)
    @ FMIBinary_lO1AZO.DeferredCCallable ~/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/c_api/ccallable.jl:68
  [5] include(mapexpr::Function, mod::Module, _path::String)
    @ Base ./Base.jl:307
  [6] top-level scope
    @ ~/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/FMIBinary_lO1AZO.jl:48
  [7] include(mod::Module, _path::String)
    @ Base ./Base.jl:306
  [8] include_package_for_output(pkg::Base.PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, concrete_deps::Vector{Pair{Base.PkgId, UInt128}}, source::Nothing)
    @ Base ./loading.jl:2997
  [9] top-level scope
    @ stdin:5
 [10] eval(m::Module, e::Any)
    @ Core ./boot.jl:489
 [11] include_string(mapexpr::typeof(identity), mod::Module, code::String, filename::String)
    @ Base ./loading.jl:2843
 [12] include_string
    @ ./loading.jl:2853 [inlined]
 [13] exec_options(opts::Base.JLOptions)
    @ Base ./client.jl:315
 [14] _start()
    @ Base ./client.jl:550
in expression starting at /home/ufechner/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/c_api/common.jl:12
in expression starting at /home/ufechner/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/c_api/common.jl:12
in expression starting at /home/ufechner/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl/src/FMIBinary_lO1AZO.jl:1
in expression starting at stdin:
ERROR: LoadError: failed process: Process(setenv(`/home/ufechner/.julia/juliaup/julia-1.12.0-rc3+0.x64.linux.gnu/bin/julia --color=yes --startup-file=no --pkgimages=no --sysimage=/tmp/jl_AEWZO1/tmp_sys.so -e 'using Pkg; Pkg.precompile()'`,["QT_ACCESSIBILITY=1", "USE_REVISE=false", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus", "XDG_SESSION_TYPE=wayland", "SYSTEMD_EXEC_PID=5967", "USER=ufechner", "XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg", "LESSCLOSE=/usr/bin/lesspipe %s %s", "LC_NUMERIC=nl_NL.UTF-8", "LC_MEASUREMENT=nl_NL.UTF-8"  …  "VSCODE_GIT_ASKPASS_MAIN=/usr/share/code/resources/app/extensions/git/dist/askpass-main.js", "ORIGINAL_XDG_CURRENT_DESKTOP=ubuntu:GNOME", "MAMBA_EXE=/home/ufechner/.julia/artifacts/87052ac9aec71548f804b30280151288cb1ed40e/bin/micromamba", "LC_NAME=nl_NL.UTF-8", "JOURNAL_STREAM=8:24568", "LC_IDENTIFICATION=nl_NL.UTF-8", "JULIA_LOAD_PATH=/home/ufechner/.julia/scratchspaces/ca28fe3e-7809-4c0f-9d3e-a21c6e6f3e9d/JSDeploymentjl/lO1AZO/FMIBinary_lO1AZO.jl:@stdlib", "GIO_LAUNCHED_DESKTOP_FILE_PID=94681", "MAMBA_ROOT_PREFIX=/home/ufechner/micromamba", "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:"]), ProcessExited(1)) [1]

Stacktrace:
  [1] pipeline_error
    @ ./process.jl:598 [inlined]
  [2] run(::Cmd; wait::Bool)
    @ Base ./process.jl:513
  [3] run
    @ ./process.jl:510 [inlined]
  [4] ensurecompiled(project::String, packages::Vector{String}, sysimage::String)
    @ PackageCompiler ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:300
  [5] create_sysimage(packages::Vector{…}; sysimage_path::String, project::String, precompile_execution_file::String, precompile_statements_file::Vector{…}, incremental::Bool, filter_stdlibs::Bool, cpu_target::String, script::String, sysimage_build_args::Cmd, include_transitive_dependencies::Bool, base_sysimage::String, julia_init_c_file::String, julia_init_h_file::Vector{…}, version::Nothing, soname::String, compat_level::String, extra_precompiles::String, import_into_main::Bool)
    @ PackageCompiler ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:595
  [6] create_sysimage
    @ ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:532 [inlined]
  [7] create_sysimage_workaround(ctx::Pkg.Types.Context, sysimage_path::String, precompile_execution_file::String, precompile_statements_file::Vector{…}, incremental::Bool, filter_stdlibs::Bool, cpu_target::String; sysimage_build_args::Cmd, include_transitive_dependencies::Bool, julia_init_c_file::String, julia_init_h_file::Vector{…}, version::Nothing, soname::String, script::String, base_sysimage::Nothing)
    @ PackageCompiler ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:1187
  [8] create_sysimage_workaround
    @ ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:1153 [inlined]
  [9] create_library(package_or_project::String, dest_dir::String; lib_name::String, precompile_execution_file::String, precompile_statements_file::Vector{…}, incremental::Bool, filter_stdlibs::Bool, force::Bool, header_files::Vector{…}, julia_init_c_file::String, julia_init_h_file::Vector{…}, version::Nothing, compat_level::String, cpu_target::String, include_lazy_artifacts::Bool, sysimage_build_args::Cmd, include_transitive_dependencies::Bool, include_preferences::Bool, script::String, base_sysimage::Nothing)
    @ PackageCompiler ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:1100
 [10] create_library
    @ ~/.julia/packages/PackageCompiler/FtDNu/src/PackageCompiler.jl:1034 [inlined]
 [11] generate_fmu_sysimage!(lib_path::String, fmu_name::String, path::FMUGeneration.var"#path#44"{…}; incremental::Bool, include_lazy_artifacts::Bool, sysimage_kwargs::@Kwargs{})
    @ FMUGeneration ~/.julia/packages/FMUGeneration/HKWrt/src/compile.jl:112
 [12] generate_fmu_sysimage!
    @ ~/.julia/packages/FMUGeneration/HKWrt/src/compile.jl:106 [inlined]
 [13] compile_fmu(fmu_name::String, fmi_version::FMUGeneration.FMIEnums.FMIVersion, lib_path::String, xml_path::String, fmu_path::String; include_lazy_artifacts::Bool, incremental::Bool, lib_path_dir::String, sysimage_kwargs::@Kwargs{})
    @ FMUGeneration ~/.julia/packages/FMUGeneration/HKWrt/src/compile.jl:56
 [14] compile_fmu
    @ ~/.julia/packages/FMUGeneration/HKWrt/src/compile.jl:38 [inlined]
 [15] compile!(fmu::JuliaFMU; fmu_path::String, kwargs::@Kwargs{})
    @ FMUGeneration ~/.julia/packages/FMUGeneration/HKWrt/src/api.jl:265
 [16] compile!
    @ ~/.julia/packages/FMUGeneration/HKWrt/src/api.jl:261 [inlined]
 [17] JuliaFMU(name::String, fmi_version::FMUGeneration.FMIEnums.FMIVersion, fmi_types::Vector{…}; default_tspan::Tuple{…}, default_stepsize::Nothing, default_tolerance::Nothing, parameters::Vector{…}, inputs::Vector{…}, states::Vector{…}, outputs::Vector{…}, dependencies::Tuple{…}, deserialization_utils::Vector{…}, objects::Vector{…}, dynamics_utils::Vector{…}, ode_function::Expr, state_initializer::Expr, observables_function::Nothing, cosimulator::Nothing, cosimulator_solver::Expr, cosimulator_odefunction_options::@NamedTuple{}, cosimulator_integrator_options::@NamedTuple{}, n_octavian_threads::Int64, build::Bool, kwargs::@Kwargs{})
    @ FMUGeneration ~/.julia/packages/FMUGeneration/HKWrt/src/api.jl:225
 [18] top-level scope
    @ ~/repos/FMU_Test/examples/first.jl:0
 [19] macro expansion
    @ ~/.julia/packages/FMUGeneration/HKWrt/src/code_generation/macros.jl:54 [inlined]
 [20] include(mapexpr::Function, mod::Module, _path::String)
    @ Base ./Base.jl:307
 [21] top-level scope
    @ REPL[4]:1
in expression starting at /home/ufechner/repos/FMU_Test/examples/first.jl:21
Some type information was truncated. Use `show(err)` to see complete types.

Using Julia 1.12-rc3 on Linux.

UPDATE

I tried again with Julia 1.11 and that works!

But then I cannot use the nice option use_juliac=true.

Guess I have to wait until Julia 1.12 is released.

Update II

I could create an FMU and import it in Simulink. But if I try to run the simulation, it crashes. Is this example FMU supposed to work with Simulink?

I use Matlab R2025a.

I have now a version that does not crash:

using FMUGeneration
using OrdinaryDiffEq

lv_expr = :(function (dx, x, u, p, t)
    x₁, x₂ = x
    u₁, u₂ = u
    alpha, beta, gamma, delta = p
    dx₁ = alpha * x₁ - beta * x₁ * x₂ + u₁
    dx₂ = delta * x₁ * x₂ - gamma * x₂ - u₂
    dx = [dx₁, dx₂]
end);

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 = ["alpha", "beta", "gamma", "delta"]
input_names = ["u_1" , "u_2"]
state_names = ["x_1" , "x_2"]
output_names = ["y_1", "y_2"]  # Different names to avoid conflict with states

# Define observable function to compute outputs
observable_expr = :(function (x, u, p, t)
    return x  # Output the states directly
end)

fmu = JuliaFMU(
    # REQUIRED ARGUMENTS

    # We specify the FMU name
    "lotka-volterra",
    # We specify the FMI version
    FMI_V2, # Changed from FMI_V3 for MATLAB compatibility
    # We specify the FMU operating types supported - only co-simulation for MATLAB compatibility
    [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)
    ],
    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 = 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
)

# Copy the generated FMU to the output directory
output_dir = joinpath(@__DIR__, "..", "output")
mkpath(output_dir)  # Create output directory if it doesn't exist

# Get the FMU file path and copy it
fmu_source_path = fmu.lib_path
fmu_filename = basename(fmu_source_path)
fmu_dest_path = joinpath(output_dir, fmu_filename)

# Also copy the model description XML if it exists
xml_source_dir = dirname(fmu_source_path)
xml_source = joinpath(xml_source_dir, "modelDescription.xml")
if isfile(xml_source)
    xml_dest = joinpath(output_dir, "modelDescription.xml")
    cp(xml_source, xml_dest, force=true)
    println("✅ Model description XML copied to: $xml_dest")
end
fmu_source_dir = dirname(fmu_source_path)
fmu_filename = "lotka-volterra.fmu"
fmu_source = joinpath(fmu_source_dir, fmu_filename)
if isfile(fmu_source)
    fmu_dest = joinpath(output_dir, fmu_filename)
    cp(fmu_source, fmu_dest, force=true)
    println("✅ FMU copied to: $fmu_dest")
end 

So I can import it in Simulink and run it.

But the output is constant, so nothing happens.

As input values I tried 1,1 and 0.01, 0.01.

@ufechner7 thanks for trying it out! By input values, are you connecting a constant block to the model?

Yes, indeed.

All outputs have a constant value of one, no matter what the input signal is.

That’s very odd. It looks like these are just initial conditions and the model did not even run.

The documentation page is also outdated Uwe. Let me ping this link with some updated documentation.

Small update: The second example works. I had to change the “communication interval” in Simulink from auto to 0.01 to make it work.

In addition, I added these lines to copy the FMU file to the desired directory:

# Save the FMU to the output directory
output_dir = joinpath(@__DIR__, "..", "output")
mkpath(output_dir)  # Create output directory if it doesn't exist
fmu_filename = basename(fmu.fmu_path)
fmu_source = fmu.fmu_path
if isfile(fmu_source)
    fmu_dest = joinpath(output_dir, fmu_filename)
    cp(fmu_source, fmu_dest, force=true)
    println("✅ FMU copied to: $fmu_dest")
end 

Further, the directory with the FMU file must be in the Matlab path.

The first example also works now, but the example in the documentation is broken. This code works:

using FMUGeneration
using OrdinaryDiffEq

lv_expr = :(function (dx, x, u, p, t)
    x1, x2 = x
    u1, u2 = u
    alpha, beta, gamma, delta = p
    dx[1] = alpha * x1 - beta * x1 * x2 + u1
    dx[2] = delta * x1 * x2 - gamma * x2 - u2
    return nothing  # In-place modification
end);

initial_states = [1.0, 1.0]
initial_inputs = [0.01, 0.02]
default_parameters = [2.0, 1.875, 2.0, 1.875]
tspan = (0.0, 10.0)
param_names = ["alpha", "beta", "gamma", "delta"]
input_names = ["u_1" , "u_2"]
state_names = ["x_1" , "x_2"]
output_names = ["y_1", "y_2"]  # Different names to avoid conflict with states

# Define observable function to compute outputs
observable_expr = :(function (x, u, p, t)
    # Return the states as individual outputs (y_1 = x_1, y_2 = x_2)
    y1 = x[1]
    y2 = x[2]
    return [y1, y2]
end)

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)
    ],
    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 = 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
)

# Save the FMU to the output directory
output_dir = joinpath(@__DIR__, "..", "output")
mkpath(output_dir)  # Create output directory if it doesn't exist
fmu_filename = basename(fmu.fmu_path)
fmu_source = fmu.fmu_path
if isfile(fmu_source)
    fmu_dest = joinpath(output_dir, fmu_filename)
    cp(fmu_source, fmu_dest, force=true)
    println("✅ FMU copied to: $fmu_dest")
end 

Key changes:

  • do not use greek letters, only ascii
  • add an output function (Simulink crashes if there is no output function)
  • fix the ODE definition. It did not modify dx in-place.

This I think we can handle with some canonicalizations. I think the xml spec of the other FMU readers probably wants ascii characters and we’ll be limited by that, so we should just convert automatically.

Interesting. I think that’s not a general requirement for FMUs? Would be good to try it in Dymola without the output function. Since in acausal environments you could still use the observable, I wonder if this is a Simulink behavoir.

No, that is a bug in Simulink. Nevertheless, it would be nice if the initial example in the documentation also worked with Simulink.

Further:
FMI 3.0 does not allow the use of arbitrary Unicode characters in the names of inputs and outputs (i.e., variable names). The variable names in the modelDescription.xml must conform to the XML type xs:NCName, which restricts allowed characters to a subset of characters, primarily those found in ASCII, though it includes support for letters, digits, underscores, and some additional marks. Unicode letters outside the allowed set for XML identifiers (which excludes many non-Latin or special Unicode characters) are not permitted in variable names

I created a bug report: Bug in FMUGeneration.jl · Issue #15 · DyadLang/DyadIssues · GitHub