Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add RealtimeClock #2414

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/clock.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,14 @@
abstract type AbstractClock <: AbstractDiscrete end

"""
Clock <: AbstractClock
Clock([t]; dt)
Clock()
Clock(dt)
Clock(t, dt)

The default periodic clock with independent variables `t` and tick interval `dt`.
If `dt` is left unspecified, it will be inferred (if possible).

See also [`RealtimeClock`](@ref).
"""
struct Clock <: AbstractClock
"Independent variable"
Expand All @@ -122,3 +125,25 @@
end

is_concrete_time_domain(x) = x isa Union{AbstractClock, Continuous}

"""
RealtimeClock()
RealtimeClock(dt)
RealtimeClock(t, dt)

Similar to [`Clock`](@ref), but with with the additional property that the simulation is run no faster than real-time, i.e., a simulation with `tspan = (0, 10)` will take at least 10 seconds of wallclock time to run.

To achieve real-time execution, the wall-clock duration of each integration step is measured, and a call to `Libc.systemsleep` is made to ensure that the next integration step is not started before `dt` seconds of wall-clock time has elapsed. This leads to a simulation that takes at least as long as the length of the time span, but may take longer if the computational load is high enough that one integration step takes more than `dt` seconds.
"""
struct RealtimeClock <: AbstractClock
clock::Clock
RealtimeClock(args...) = new(Clock(args...))

Check warning on line 140 in src/clock.jl

View check run for this annotation

Codecov / codecov/patch

src/clock.jl#L140

Added line #L140 was not covered by tests
end

sampletime(c) = sampletime(c.clock)
Base.hash(c::RealtimeClock, seed::UInt) = hash(c.dt, seed ⊻ 0x9d3d7a9a18874b90)
Base.:(==)(c1::RealtimeClock, c2::RealtimeClock) = c1.clock == c2.clock
function Base.getproperty(c::RealtimeClock, s::Symbol)
s ∈ fieldnames(typeof(c)) && return getfield(c, s)
getproperty(getfield(c, :clock), s)

Check warning on line 148 in src/clock.jl

View check run for this annotation

Codecov / codecov/patch

src/clock.jl#L143-L148

Added lines #L143 - L148 were not covered by tests
end
25 changes: 25 additions & 0 deletions src/systems/clock_inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,28 @@
defaults = Dict{Any, Any}(v => 0.0 for v in Iterators.flatten(inputs))
return affects, clocks, svs, appended_parameters, defaults
end

function generate_discrete_callback(affect, clock, sv)
error("$clock is not a supported clock type.")

Check warning on line 241 in src/systems/clock_inference.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/clock_inference.jl#L240-L241

Added lines #L240 - L241 were not covered by tests
end # Fallback

function generate_discrete_callback(affect, clock::Clock, sv)
PeriodicCallback(DiscreteSaveAffect(affect, sv), clock.dt)

Check warning on line 245 in src/systems/clock_inference.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/clock_inference.jl#L244-L245

Added lines #L244 - L245 were not covered by tests
end

function generate_discrete_callback(affect, clock::RealtimeClock, sv)
start_time = time() # Set start time before simulation starts
function realtime_affect!(integrator, saved_values)

Check warning on line 250 in src/systems/clock_inference.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/clock_inference.jl#L248-L250

Added lines #L248 - L250 were not covered by tests
# This is a closure that closes over start_time
affect(integrator, saved_values)
execution_time = time() - start_time
sleep_time = clock.dt - execution_time
if sleep_time < 0
@debug "RealtimeClock failed to meet deadline dt = $(clock.dt), execution time = $execution_time at simulation time t = $(t)"

Check warning on line 256 in src/systems/clock_inference.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/clock_inference.jl#L252-L256

Added lines #L252 - L256 were not covered by tests
end
Libc.systemsleep(max(0, sleep_time))
start_time = time() # Update start time to current time
nothing

Check warning on line 260 in src/systems/clock_inference.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/clock_inference.jl#L258-L260

Added lines #L258 - L260 were not covered by tests
end
PeriodicCallback(DiscreteSaveAffect(realtime_affect!, sv), clock.dt)

Check warning on line 262 in src/systems/clock_inference.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/clock_inference.jl#L262

Added line #L262 was not covered by tests
end
8 changes: 1 addition & 7 deletions src/systems/diffeqs/abstractodesystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -947,13 +947,7 @@
cbs = process_events(sys; callback, has_difference, kwargs...)
if has_discrete_subsystems(sys) && (dss = get_discrete_subsystems(sys)) !== nothing
affects, clocks, svs = ModelingToolkit.generate_discrete_affect(dss...)
discrete_cbs = map(affects, clocks, svs) do affect, clock, sv
if clock isa Clock
PeriodicCallback(DiscreteSaveAffect(affect, sv), clock.dt)
else
error("$clock is not a supported clock type.")
end
end
discrete_cbs = map(ModelingToolkit.generate_discrete_callback, affects, clocks, svs)

Check warning on line 950 in src/systems/diffeqs/abstractodesystem.jl

View check run for this annotation

Codecov / codecov/patch

src/systems/diffeqs/abstractodesystem.jl#L950

Added line #L950 was not covered by tests
if cbs === nothing
if length(discrete_cbs) == 1
cbs = only(discrete_cbs)
Expand Down
23 changes: 23 additions & 0 deletions test/clock.jl
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,26 @@ if VERSION >= v"1.7"

@test sol.u ≈ sol2.u
end

# RealtimeClock
dt = 0.01
@variables t x(t)=0 y(t)=0 u(t)=0 yd(t)=0 ud(t)=0 r(t)=1
@parameters kp = 1
D = Differential(t)
d = ModelingToolkit.RealtimeClock(dt)
# u(n + 1) := f(u(n))

eqs = [yd ~ Sample(d)(y)
ud ~ kp * (r - yd)
r ~ 1.0

# plant (time continuous part)
u ~ Hold(ud)
D(x) ~ -x + u
y ~ x]
@named sys = ODESystem(eqs)
prob = ODEProblem(structural_simplify(sys), [], (0.0, 3.0))
solve(prob, Tsit5(), kwargshandle = KeywordArgSilent)
execution_time = @elapsed solve(prob, Tsit5(), kwargshandle = KeywordArgSilent)
@test_skip execution_time≈3 atol=10 * dt # Very crude test that can be evaluated locally but not on CI
@test execution_time >= 3 - dt # This can be evaluated on CI as well since it only tests a lower bound
Loading