Skip to content

refactor: allow new clocks to be defined in downstream packages #995

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

Merged
merged 7 commits into from
Aug 6, 2025
Merged
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
1 change: 1 addition & 0 deletions src/SciMLBase.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ADTypes: ADTypes, AbstractADType
import Accessors: @set, @reset, @delete, @insert
using Moshi.Data: @data
using Moshi.Match: @match
import Moshi.Derive: @derive
import StaticArraysCore
import Adapt: adapt_structure, adapt

Expand Down
82 changes: 72 additions & 10 deletions src/clock.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
@data Clocks begin
abstract type AbstractClock end

@data Clocks<:AbstractClock begin
ContinuousClock
struct PeriodicClock
dt::Union{Nothing, Float64, Rational{Int}}
phase::Float64 = 0.0
end
SolverStepClock
struct EventClock
id::Symbol
end
end

@derive Clocks[Show, Hash, Eq]

# for backwards compatibility
const TimeDomain = Clocks.Type
using .Clocks: ContinuousClock, PeriodicClock, SolverStepClock
const TimeDomain = AbstractClock
using .Clocks: ContinuousClock, PeriodicClock, SolverStepClock, EventClock
const Continuous = ContinuousClock()
(clock::TimeDomain)() = clock

Expand Down Expand Up @@ -40,44 +47,99 @@ discrete-time systems that assume a fixed sample time, such as PID controllers a
filters.
""" SolverStepClock

isclock(c::TimeDomain) = @match c begin
isclock(c::Clocks.Type) = @match c begin
PeriodicClock() => true
_ => false
end
isclock(::TimeDomain) = false

issolverstepclock(c::TimeDomain) = @match c begin
issolverstepclock(c::Clocks.Type) = @match c begin
SolverStepClock() => true
_ => false
end
issolverstepclock(::TimeDomain) = false

iscontinuous(c::TimeDomain) = @match c begin
iscontinuous(c::Clocks.Type) = @match c begin
ContinuousClock() => true
_ => false
end
iscontinuous(::TimeDomain) = false

iseventclock(c::Clocks.Type) = @match c begin
EventClock() => true
_ => false
end
iseventclock(::TimeDomain) = false

is_discrete_time_domain(c::TimeDomain) = !iscontinuous(c)

# workaround for https://github.com/Roger-luo/Moshi.jl/issues/43
isclock(::Any) = false
issolverstepclock(::Any) = false
iscontinuous(::Any) = false
iseventclock(::Any) = false
is_discrete_time_domain(::Any) = false

function first_clock_tick_time(c, t0)
# public
function first_clock_tick_time(c::Clocks.Type, t0)
@match c begin
PeriodicClock(dt) => ceil(t0 / dt) * dt
SolverStepClock() => t0
ContinuousClock() => error("ContinuousClock() is not a discrete clock")
EventClock() => error("Event clocks do not have a defined first tick time.")
_ => error("Unimplemented for clock $c")
end
end

struct IndexedClock{I}
clock::TimeDomain
function first_clock_tick_time(c::TimeDomain, _)
error("Unimplemented for clock $c")
end

# public
"""
$(TYPEDEF)

A struct representing the operation of indexing a clock to obtain a subset of the time
points at which it ticked. The actual list of time points depends on the tick instances
on which the clock was ticking, and can be obtained via `canonicalize_indexed_clock`
by providing a timeseries solution object.

For example, `IndexedClock(PeriodicClock(0.1), 3)` refers to the third time that
`PeriodicClock(0.1)` ticked. If the simulation started at `t = 0`, then this would be
`t = 0.2`. Similarly, `IndexedClock(PeriodicClock(0.1), [1, 5])` refers to `t = 0.0`
and `t = 0.4` in this context.

# Fields

$(TYPEDFIELDS)
"""
struct IndexedClock{C <: AbstractClock, I}
"""
The clock being indexed. A subtype of `SciMLBase.AbstractClock`
"""
clock::C
"""
The subset of indexes being referred to. This can be an integer, an array of integers,
a range or `Colon()` to refer to all the points that the clock ticked.
"""
idx::I
end

Base.getindex(c::TimeDomain, idx) = IndexedClock(c, idx)
# public
"""
$(TYPEDSIGNATURES)

Return a `SciMLBase.IndexedClock` representing the subset of the time points that the clock
ticked indicated by `idx`.
"""
Base.getindex(c::AbstractClock, idx) = IndexedClock(c, idx)

# public
"""
$(TYPEDSIGNATURES)

Return the time points in the interval
"""
function canonicalize_indexed_clock(ic::IndexedClock, sol::AbstractTimeseriesSolution)
c = ic.clock

Expand Down
Loading