Skip to content

Commit 88e99b1

Browse files
feat: support v3 ME FMUs
1 parent cdb2e1f commit 88e99b1

File tree

2 files changed

+153
-18
lines changed

2 files changed

+153
-18
lines changed

ext/MTKFMIExt.jl

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,46 @@ macro statuscheck(expr)
1414
fnname = fn.args[2]
1515

1616
instance = expr.args[2]
17-
17+
is_v2 = startswith("fmi2", string(fnname))
18+
19+
fmiTrue = is_v2 ? FMI.fmi2True : FMI.fmi3True
20+
fmiStatusOK = is_v2 ? FMI.fmi2StatusOK : FMI.fmi3StatusOK
21+
fmiStatusWarning = is_v2 ? FMI.fmi2StatusWarning : FMI.fmi3StatusWarning
22+
fmiStatusFatal = is_v2 ? FMI.fmi2StatusFatal : FMI.fmi3StatusFatal
23+
fmiTerminate = is_v2 ? FMI.fmi2Terminate : FMI.fmi3Terminate
24+
fmiFreeInstance! = is_v2 ? FMI.fmi2FreeInstance! : FMI.fmi3FreeInstance!
1825
return quote
1926
status = $expr
2027
fnname = $fnname
21-
if status !== nothing && ((status isa Tuple && status[1] == FMI.fmi2True) ||
22-
(!(status isa Tuple) && status != FMI.fmi2StatusOK &&
23-
status != FMI.fmi2StatusWarning))
24-
if status != FMI.fmi2StatusFatal
25-
FMI.fmi2Terminate(wrapper.instance)
28+
if status !== nothing && ((status isa Tuple && status[1] == $fmiTrue) ||
29+
(!(status isa Tuple) && status != $fmiStatusOK &&
30+
status != $fmiStatusWarning))
31+
if status != $fmiStatusFatal
32+
$fmiTerminate(wrapper.instance)
2633
end
27-
FMI.fmi2FreeInstance!(wrapper.instance)
34+
$fmiFreeInstance!(wrapper.instance)
2835
wrapper.instance = nothing
2936
error("FMU Error in $fnname: status $status")
3037
end
3138
end |> esc
3239
end
3340

34-
function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6,
35-
communication_step_size = nothing, type, name)
41+
@static if !hasmethod(FMI.getValueReferencesAndNames, Tuple{FMI.fmi3ModelDescription})
42+
function FMI.getValueReferencesAndNames(
43+
md::FMI.fmi3ModelDescription; vrs = md.valueReferences)
44+
dict = Dict{FMI.fmi3ValueReference, Array{String}}()
45+
for vr in vrs
46+
dict[vr] = FMI.valueReferenceToString(md, vr)
47+
end
48+
return dict
49+
end
50+
end
51+
52+
function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6,
53+
communication_step_size = nothing, type, name) where {Ver}
54+
if Ver != 2 && Ver != 3
55+
throw(ArgumentError("FMI Version must be `2` or `3`"))
56+
end
3657
if type == :CS && communication_step_size === nothing
3758
throw(ArgumentError("`communication_step_size` must be specified for Co-Simulation FMUs."))
3859
end
@@ -90,16 +111,23 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6,
90111

91112
input_value_references = UInt32[value_references[var] for var in inputs]
92113
param_value_references = UInt32[value_references[var] for var in params]
93-
@parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper(
94-
fmu, param_value_references, input_value_references, tolerance)
114+
115+
if Ver == 2
116+
@parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper(
117+
fmu, param_value_references, input_value_references, tolerance)
118+
else
119+
@parameters wrapper::FMI3InstanceWrapper = FMI3InstanceWrapper(
120+
fmu, param_value_references, input_value_references)
121+
end
95122

96123
output_value_references = UInt32[value_references[var] for var in outputs]
97124
buffer_length = length(diffvars) + length(outputs)
98125

99126
initialization_eqs = Equation[]
100127

101128
if type == :ME
102-
_functor = FMI2MEFunctor(zeros(buffer_length), output_value_references)
129+
FunctorT = Ver == 2 ? FMI2MEFunctor : FMI3MEFunctor
130+
_functor = FunctorT(zeros(buffer_length), output_value_references)
103131
@parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor
104132
call_expr = functor(
105133
wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t)
@@ -109,14 +137,14 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6,
109137
push!(diffeqs, var ~ call_expr[i])
110138
end
111139

112-
finalize_affect = MTK.FunctionalAffect(fmi2Finalize!, [], [wrapper], [])
113-
step_affect = MTK.FunctionalAffect(fmi2MEStep!, [], [wrapper], [])
140+
finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], [])
141+
step_affect = MTK.FunctionalAffect(fmiMEStep!, [], [wrapper], [])
114142
instance_management_callback = MTK.SymbolicDiscreteCallback(
115143
(t != t - 1), step_affect; finalize = finalize_affect)
116144

117145
push!(params, wrapper, functor)
118146
push!(states, __mtk_internal_u)
119-
elseif type == :CS
147+
elseif type == :CS && Ver == 2
120148
state_value_references = UInt32[value_references[var] for var in diffvars]
121149
state_and_output_value_references = vcat(
122150
state_value_references, output_value_references)
@@ -142,7 +170,7 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6,
142170
end
143171
initialize_affect = MTK.ImperativeAffect(fmi2CSInitialize!; observed = cb_observed,
144172
modified = cb_modified, ctx = _functor)
145-
finalize_affect = MTK.FunctionalAffect(fmi2Finalize!, [], [wrapper], [])
173+
finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], [])
146174
step_affect = MTK.ImperativeAffect(
147175
fmi2CSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor)
148176
instance_management_callback = MTK.SymbolicDiscreteCallback(
@@ -250,6 +278,63 @@ function reset_instance!(wrapper::FMI2InstanceWrapper)
250278
wrapper.instance = nothing
251279
end
252280

281+
mutable struct FMI3InstanceWrapper
282+
const fmu::FMI.FMU3
283+
const param_value_references::Vector{UInt32}
284+
const input_value_references::Vector{UInt32}
285+
instance::Union{FMI.FMU3Instance{FMI.FMU3}, Nothing}
286+
end
287+
288+
function FMI3InstanceWrapper(fmu, params, inputs)
289+
FMI3InstanceWrapper(fmu, params, inputs, nothing)
290+
end
291+
292+
function get_instance_common!(wrapper::FMI3InstanceWrapper, states, inputs, params, t)
293+
if !isempty(params)
294+
@statuscheck FMI.fmi3SetFloat64(wrapper.instance, wrapper.param_value_references,
295+
params)
296+
end
297+
@statuscheck FMI.fmi3EnterInitializationMode(
298+
wrapper.instance, FMI.fmi3False, zero(FMI.fmi3Float64), t, FMI.fmi3False, t)
299+
if !isempty(inputs)
300+
@statuscheck FMI.fmi3SetFloat64(
301+
wrapper.instance, wrapper.input_value_references, inputs)
302+
end
303+
304+
return wrapper.instance
305+
end
306+
307+
function get_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, params, t)
308+
if wrapper.instance === nothing
309+
wrapper.instance = FMI.fmi3InstantiateModelExchange!(wrapper.fmu)::FMI.FMU3Instance
310+
get_instance_common!(wrapper, states, inputs, params, t)
311+
@statuscheck FMI.fmi3ExitInitializationMode(wrapper.instance)
312+
eventInfo = FMI.fmi3UpdateDiscreteStates(wrapper.instance)
313+
@assert eventInfo[1] == FMI.fmi2False
314+
# TODO: Support FMU events
315+
@statuscheck FMI.fmi3EnterContinuousTimeMode(wrapper.instance)
316+
end
317+
318+
return wrapper.instance
319+
end
320+
321+
function complete_step!(wrapper::FMI3InstanceWrapper)
322+
wrapper.instance === nothing && return
323+
enterEventMode = Ref(FMI.fmi3False)
324+
terminateSimulation = Ref(FMI.fmi3False)
325+
@statuscheck FMI.fmi3CompletedIntegratorStep!(
326+
wrapper.instance, FMI.fmi3True, enterEventMode, terminateSimulation)
327+
@assert enterEventMode[] == FMI.fmi3False
328+
@assert terminateSimulation[] == FMI.fmi3False
329+
end
330+
331+
function reset_instance!(wrapper::FMI3InstanceWrapper)
332+
wrapper.instance === nothing && return
333+
FMI.fmi3Terminate(wrapper.instance)
334+
FMI.fmi3FreeInstance!(wrapper.instance)
335+
wrapper.instance = nothing
336+
end
337+
253338
struct FMI2MEFunctor{T}
254339
return_buffer::Vector{T}
255340
output_value_references::Vector{UInt32}
@@ -284,13 +369,46 @@ function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, param
284369
return [states_buffer; outputs_buffer]
285370
end
286371

287-
function fmi2MEStep!(integrator, u, p, ctx)
372+
struct FMI3MEFunctor{T}
373+
return_buffer::Vector{T}
374+
output_value_references::Vector{UInt32}
375+
end
376+
377+
@register_array_symbolic (fn::FMI3MEFunctor)(
378+
wrapper::FMI3InstanceWrapper, states::Vector{<:Real},
379+
inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin
380+
size = (length(states) + length(fn.output_value_references),)
381+
eltype = eltype(states)
382+
ndims = 1
383+
end
384+
385+
function update_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, t)
386+
instance = wrapper.instance
387+
@statuscheck FMI.fmi3SetTime(instance, t)
388+
@statuscheck FMI.fmi3SetContinuousStates(instance, states)
389+
if !isempty(inputs)
390+
@statuscheck FMI.fmi3SetFloat64(instance, wrapper.input_value_references, inputs)
391+
end
392+
end
393+
394+
function (fn::FMI3MEFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, params, t)
395+
instance = get_instance_ME!(wrapper, states, inputs, params, t)
396+
update_instance_ME!(wrapper, states, inputs, t)
397+
398+
states_buffer = zeros(length(states))
399+
@statuscheck FMI.fmi3GetContinuousStateDerivatives!(instance, states_buffer)
400+
outputs_buffer = zeros(length(fn.output_value_references))
401+
FMI.fmi3GetFloat64!(instance, fn.output_value_references, outputs_buffer)
402+
return [states_buffer; outputs_buffer]
403+
end
404+
405+
function fmiMEStep!(integrator, u, p, ctx)
288406
wrapper_idx = p[1]
289407
wrapper = integrator.ps[wrapper_idx]
290408
complete_step!(wrapper)
291409
end
292410

293-
function fmi2Finalize!(integrator, u, p, ctx)
411+
function fmiFinalize!(integrator, u, p, ctx)
294412
wrapper_idx = p[1]
295413
wrapper = integrator.ps[wrapper_idx]
296414
reset_instance!(wrapper)

test/extensions/fmi.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ import ModelingToolkit as MTK
3535
@test sol(0.0:0.1:8.0;
3636
idxs = [sys.inner.mass__s, sys.inner.mass__v]).ucollect.(truesol.values.saveval) rtol=1e-2
3737
end
38+
39+
fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME)
40+
truesol = FMI.simulate(
41+
fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0, recordValues = ["mass.s", "mass.v"])
42+
@testset "v3, ME" begin
43+
fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME)
44+
@mtkbuild sys = MTK.FMIComponent(Val(3); fmu, type = :ME)
45+
prob = ODEProblem{true, SciMLBase.FullSpecialize}(
46+
sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0))
47+
sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8)
48+
@test SciMLBase.successful_retcode(sol)
49+
50+
@test sol(0.0:0.1:8.0;
51+
idxs = [sys.mass__s, sys.mass__v]).ucollect.(truesol.values.saveval) atol=1e-4
52+
# repeated solve works
53+
@test_nowarn solve(prob, Tsit5())
54+
end
3855
end
3956

4057
@testset "IO Model" begin

0 commit comments

Comments
 (0)