Skip to content

Commit 3712fef

Browse files
asinghvi17claude
andcommitted
style(fmi): typeassert the metadata read, capture the callback closures in a let
`getmetadata` for `FMUEventMetadata` runs only for a parameter that carries it, so the result is asserted `::FMUEventMetadata`. `no_event_access` and `get_inputs` are assigned after their initialization, which boxed them in every callback closure that reads them: eight `Core.Box` fields across the state-event condition and affect, the step affect and its initialize. The closures now capture through an explicit `let`, so the boxes are gone from the root-finding path as well. The `CallbackConstructionHook` read in `generate_hook_callbacks` is left alone: the hook is an arbitrary user callable with no type to assert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e33c5d3 commit 3712fef

1 file changed

Lines changed: 122 additions & 115 deletions

File tree

ext/MTKFMIExt.jl

Lines changed: 122 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ them; one that declares none does not.
240240
function build_fmu_me_callbacks(sys, wrapper_param)
241241
meta = SymbolicUtils.getmetadata(
242242
SymbolicUtils.unwrap(wrapper_param), FMUEventMetadata
243-
)
243+
)::FMUEventMetadata
244244
get_wrapper = SII.getp(sys, wrapper_param)
245245
# `nothing` if the integrator can exchange state and inputs with the FMU, else the reason
246246
# it cannot. An event needs both: states to write the post-event values back into, and
@@ -290,139 +290,146 @@ function build_fmu_me_callbacks(sys, wrapper_param)
290290
state_buffer = zeros(Float64, length(state_idxs))
291291
input_buffer = zeros(Float64, length(input_names))
292292

293-
function push_fmu_event_state!(wrapper, u, p, t)
294-
for (i, idx) in enumerate(state_idxs)
295-
state_buffer[i] = u[idx]
296-
end
297-
if get_inputs !== nothing
298-
copyto!(input_buffer, get_inputs(u, p, t))
293+
# the closures below are the callbacks. They capture with an explicit `let` because
294+
# `no_event_access` and `get_inputs` are assigned above after their initialization, which
295+
# would otherwise box them in the capture.
296+
return let get_wrapper = get_wrapper, wrapper_param = wrapper_param,
297+
no_event_access = no_event_access, get_inputs = get_inputs,
298+
state_idxs = state_idxs, state_buffer = state_buffer, input_buffer = input_buffer
299+
function push_fmu_event_state!(wrapper, u, p, t)
300+
for (i, idx) in enumerate(state_idxs)
301+
state_buffer[i] = u[idx]
302+
end
303+
if get_inputs !== nothing
304+
copyto!(input_buffer, get_inputs(u, p, t))
305+
end
306+
return force_set_fmu_state!(wrapper, state_buffer, input_buffer, t)
299307
end
300-
return force_set_fmu_state!(wrapper, state_buffer, input_buffer, t)
301-
end
302308

303-
function fmu_event_condition!(out, u, t, integrator)
304-
wrapper = get_wrapper(integrator)
305-
# the instance is created by the first evaluation of the FMU, and unlike a completed
306-
# step an event indicator has no value without one
307-
if wrapper.instance === nothing
308-
error(
309-
"The event indicators of the FMU wrapped by $(getname(wrapper_param)) cannot \
310-
be evaluated before the FMU is instantiated, which the first evaluation of \
311-
its dynamics does."
312-
)
309+
function fmu_event_condition!(out, u, t, integrator)
310+
wrapper = get_wrapper(integrator)
311+
# the instance is created by the first evaluation of the FMU, and unlike a completed
312+
# step an event indicator has no value without one
313+
if wrapper.instance === nothing
314+
error(
315+
"The event indicators of the FMU wrapped by $(getname(wrapper_param)) cannot \
316+
be evaluated before the FMU is instantiated, which the first evaluation of \
317+
its dynamics does."
318+
)
319+
end
320+
push_fmu_event_state!(wrapper, u, integrator.p, t)
321+
return get_fmu_event_indicators!(wrapper, out)
313322
end
314-
push_fmu_event_state!(wrapper, u, integrator.p, t)
315-
return get_fmu_event_indicators!(wrapper, out)
316-
end
317323

318-
function fmu_event_affect!(integrator, idx)
319-
wrapper = get_wrapper(integrator)
320-
push_fmu_event_state!(wrapper, integrator.u, integrator.p, integrator.t)
321-
enter_fmu_event_mode!(wrapper, idx)
322-
event_result = do_fmu_event_iteration!(wrapper)
323-
if event_result.terminate
324-
SciMLBase.terminate!(integrator)
325-
# we suppress the discontinuity because a terminating FMU stays in Event Mode,
326-
# where the calls the interpolant rebuild makes are illegal. Nothing is stepped
327-
# after `terminate!`, so nothing needs that rebuild.
328-
SciMLBase.derivative_discontinuity!(integrator, false)
329-
return nothing
330-
end
331-
states = leave_fmu_event_mode!(wrapper, event_result.values_changed)
332-
if states !== nothing
333-
for (i, state_idx) in enumerate(state_idxs)
334-
integrator.u[state_idx] = states[i]
324+
function fmu_event_affect!(integrator, idx)
325+
wrapper = get_wrapper(integrator)
326+
push_fmu_event_state!(wrapper, integrator.u, integrator.p, integrator.t)
327+
enter_fmu_event_mode!(wrapper, idx)
328+
event_result = do_fmu_event_iteration!(wrapper)
329+
if event_result.terminate
330+
SciMLBase.terminate!(integrator)
331+
# we suppress the discontinuity because a terminating FMU stays in Event Mode,
332+
# where the calls the interpolant rebuild makes are illegal. Nothing is stepped
333+
# after `terminate!`, so nothing needs that rebuild.
334+
SciMLBase.derivative_discontinuity!(integrator, false)
335+
return nothing
335336
end
337+
states = leave_fmu_event_mode!(wrapper, event_result.values_changed)
338+
if states !== nothing
339+
for (i, state_idx) in enumerate(state_idxs)
340+
integrator.u[state_idx] = states[i]
341+
end
342+
end
343+
wrapper.next_event_time = event_result.next_event_time
344+
handle_fmu_time_event!(wrapper, event_result.next_event_time)
345+
return nothing
336346
end
337-
wrapper.next_event_time = event_result.next_event_time
338-
handle_fmu_time_event!(wrapper, event_result.next_event_time)
339-
return nothing
340-
end
341347

342-
# notify the FMU of the step and return whether it asks for Event Mode, which is the only
343-
# outcome its caller has to treat as a discontinuity
344-
function fmu_step_event_occurred!(integrator)
345-
# we bail unless the solve is in flight, because SciML still applies discrete callbacks
346-
# after a continuous one terminated, and a terminated FMU is in Event Mode where none
347-
# of the calls below are legal.
348-
retcode = integrator.sol.retcode
349-
# `check_error!` stores `Success` mid-solve, so these are the two in-flight codes
350-
in_flight = retcode === SciMLBase.ReturnCode.Default ||
351-
retcode === SciMLBase.ReturnCode.Success
352-
in_flight || return false
353-
wrapper = get_wrapper(integrator)
354-
# the instance is created by the first evaluation of the FMU, which an FMU that only
355-
# feeds observed equations may not have seen by the time this initializes
356-
wrapper.instance === nothing && return false
357-
# we push the accepted state because error control and (for an implicit solver) the
358-
# Jacobian leave the FMU at other points. Without exchangeable states there is nothing
359-
# to push, which is what FMI.jl's `stepCompleted` does for every FMU.
360-
no_event_access === nothing &&
361-
push_fmu_event_state!(wrapper, integrator.u, integrator.p, integrator.t)
362-
step_result = partiallyCompleteIntegratorStep(wrapper)
363-
if step_result.terminate
364-
SciMLBase.terminate!(integrator)
365-
return false
348+
# notify the FMU of the step and return whether it asks for Event Mode, which is the only
349+
# outcome its caller has to treat as a discontinuity
350+
function fmu_step_event_occurred!(integrator)
351+
# we bail unless the solve is in flight, because SciML still applies discrete callbacks
352+
# after a continuous one terminated, and a terminated FMU is in Event Mode where none
353+
# of the calls below are legal.
354+
retcode = integrator.sol.retcode
355+
# `check_error!` stores `Success` mid-solve, so these are the two in-flight codes
356+
in_flight = retcode === SciMLBase.ReturnCode.Default ||
357+
retcode === SciMLBase.ReturnCode.Success
358+
in_flight || return false
359+
wrapper = get_wrapper(integrator)
360+
# the instance is created by the first evaluation of the FMU, which an FMU that only
361+
# feeds observed equations may not have seen by the time this initializes
362+
wrapper.instance === nothing && return false
363+
# we push the accepted state because error control and (for an implicit solver) the
364+
# Jacobian leave the FMU at other points. Without exchangeable states there is nothing
365+
# to push, which is what FMI.jl's `stepCompleted` does for every FMU.
366+
no_event_access === nothing &&
367+
push_fmu_event_state!(wrapper, integrator.u, integrator.p, integrator.t)
368+
step_result = partiallyCompleteIntegratorStep(wrapper)
369+
if step_result.terminate
370+
SciMLBase.terminate!(integrator)
371+
return false
372+
end
373+
return step_result.enter_event_mode
366374
end
367-
return step_result.enter_event_mode
368-
end
369375

370-
function fmu_step_completed!(integrator)
371-
if fmu_step_event_occurred!(integrator)
372-
if no_event_access !== nothing
373-
error(
374-
"The FMU declares no event indicators but requested Event Mode from a \
375-
completed integrator step, which needs the same access to its states as \
376-
a state event. $no_event_access"
377-
)
376+
function fmu_step_completed!(integrator)
377+
if fmu_step_event_occurred!(integrator)
378+
if no_event_access !== nothing
379+
error(
380+
"The FMU declares no event indicators but requested Event Mode from a \
381+
completed integrator step, which needs the same access to its states as \
382+
a state event. $no_event_access"
383+
)
384+
end
385+
# we set the flag ourselves because every callback ahead of this one clears it
386+
# during the callback-initialize phase (`SciMLBase.INITIALIZE_DEFAULT`). On the
387+
# `apply_discrete_callback!` path it is already true.
388+
SciMLBase.derivative_discontinuity!(integrator, true)
389+
# a step event has no triggered event indicator
390+
return fmu_event_affect!(integrator, nothing)
378391
end
379-
# we set the flag ourselves because every callback ahead of this one clears it
380-
# during the callback-initialize phase (`SciMLBase.INITIALIZE_DEFAULT`). On the
381-
# `apply_discrete_callback!` path it is already true.
382-
SciMLBase.derivative_discontinuity!(integrator, true)
383-
# a step event has no triggered event indicator
384-
return fmu_event_affect!(integrator, nothing)
392+
# we clear the flag on every non-event path because the assumed discontinuity
393+
# reinitializes a singular-mass-matrix system and overwrites `u`, while a step the FMU
394+
# accepts as-is changed nothing.
395+
SciMLBase.derivative_discontinuity!(integrator, false)
396+
return nothing
385397
end
386-
# we clear the flag on every non-event path because the assumed discontinuity
387-
# reinitializes a singular-mass-matrix system and overwrites `u`, while a step the FMU
388-
# accepts as-is changed nothing.
389-
SciMLBase.derivative_discontinuity!(integrator, false)
390-
return nothing
391-
end
392398

393-
function fmu_step_initialize(cb, u, t, integrator)
394-
return fmu_step_completed!(integrator)
395-
end
399+
function fmu_step_initialize(cb, u, t, integrator)
400+
return fmu_step_completed!(integrator)
401+
end
396402

397-
# we name an algorithm on both callbacks because a standing discontinuity reinitializes the
398-
# integrator, and the problem's own `OverrideInit` would reset `u` to `u0` and throw away
399-
# the post-event states the affect just wrote. This one keeps the differential states and
400-
# re-solves only the algebraic variables.
401-
event_initializealg = DiffEqBase.BrownFullBasicInit()
403+
# we name an algorithm on both callbacks because a standing discontinuity reinitializes the
404+
# integrator, and the problem's own `OverrideInit` would reset `u` to `u0` and throw away
405+
# the post-event states the affect just wrote. This one keeps the differential states and
406+
# re-solves only the algebraic variables.
407+
event_initializealg = DiffEqBase.BrownFullBasicInit()
402408

403-
callbacks = SciMLBase.DECallback[]
404-
if meta.n_event_indicators > 0
409+
callbacks = SciMLBase.DECallback[]
410+
if meta.n_event_indicators > 0
411+
push!(
412+
callbacks,
413+
SciMLBase.VectorContinuousCallback(
414+
fmu_event_condition!, fmu_event_affect!, meta.n_event_indicators;
415+
# we need RightRootFind because the FMU has to be past the switch when it
416+
# enters Event Mode: https://fmi-standard.org/docs/3.0.2/#state-event
417+
rootfind = SciMLBase.RightRootFind, interp_points = 10,
418+
save_positions = (true, true), initializealg = event_initializealg
419+
)
420+
)
421+
end
405422
push!(
406423
callbacks,
407-
SciMLBase.VectorContinuousCallback(
408-
fmu_event_condition!, fmu_event_affect!, meta.n_event_indicators;
409-
# we need RightRootFind because the FMU has to be past the switch when it
410-
# enters Event Mode: https://fmi-standard.org/docs/3.0.2/#state-event
411-
rootfind = SciMLBase.RightRootFind, interp_points = 10,
412-
save_positions = (true, true), initializealg = event_initializealg
424+
SciMLBase.DiscreteCallback(
425+
Returns(true), fmu_step_completed!;
426+
# `initialize` gives the FMU the `t0` step FMI.jl's `func_start` does
427+
initialize = fmu_step_initialize, save_positions = (false, false),
428+
initializealg = event_initializealg
413429
)
414430
)
431+
callbacks
415432
end
416-
push!(
417-
callbacks,
418-
SciMLBase.DiscreteCallback(
419-
Returns(true), fmu_step_completed!;
420-
# `initialize` gives the FMU the `t0` step FMI.jl's `func_start` does
421-
initialize = fmu_step_initialize, save_positions = (false, false),
422-
initializealg = event_initializealg
423-
)
424-
)
425-
return callbacks
426433
end
427434

428435
"""

0 commit comments

Comments
 (0)