Events
Events are computations $\,α,β,γ,...\,$ at times $\,t_1,t_2,t_3,...$ We call those computations actions.
Actions
Actions are Julia functions or expressions to be executed later:
DiscreteEvents.Action
— TypeAction
An action is either a Function
or an Expr
or a Tuple
of them. It can be scheduled in an event for later execution.
DiscreteEvents.fun
— Functionfun(f::Function, args...; kwargs...)
Save a function f
and its arguments in a closure for later execution.
Arguments
The arguments args...
and keyword arguments kwargs...
to fun are passed to f
at execution but may change their values between beeing captured in fun
and f
s later execution. If f
needs their current values at execution time there are two possibilities:
fun
can takefun
s, function closures, symbols or expressions at the place of values or variable arguments. They are evaluated at event time just before being passed to f. There is one exception: iff
is anevent!
, its arguments are passed on unevaluated.- A mutable type argument (Array, struct ...) is always current. You can also change its content from within a function.
If using Symbol
s or Expr
in fun
you get a one time warning. They are evaluated at global scope in Module Main
only and therefore cannot be used by other modules.
Actions can be combined into tuples:
julia> using DiscreteEvents
julia> a = 1
1
julia> println(a) isa Action # a function call is not an action
1
false
julia> fun(println, a) isa Action # wrapped in a fun it is an action
true
julia> isa(()->println(a), Action) # an anonymous function
true
julia> :(println(a)) isa Action # an expression
true
julia> (()->println(a), fun(println, a), :(println(a))) isa Action # a tuple of them
true
Expressions and Symbols
Expressions too are Actions. Also you can pass symbols to fun
to delay evaluation of variables. Expr
s and Symbol
s are evaluated at global scope in Module Main
only. This is a user convenience feature. Other modules using DiscreteEvents
cannot use them in events and have to use functions.
Usage of Expr
or Symbol
will generate a one time warning. You can replace them easily with fun
s or function closures.
Timed Events
Actions can be scheduled as events at given times:
DiscreteEvents.Timing
— TypeTiming
Enumeration type for scheduling events and timed conditions:
at
: schedule an event at a given time,after
: schedule an event a given time after current time,every
: schedule an event every given time from now on,before
: a timed condition is true before a given time,until
: delay until t.
DiscreteEvents.event!
— Methodevent!([clk], ex, t; <keyword arguments>)
event!([clk], ex, t, cy; <keyword arguments>)
event!([clk], ex, T, t; <keyword arguments>)
Schedule an event for a given time t
.
If t
is a Distribution
, event time is evaluated as rand(t)
. If cy
is a Distribution
, the event is repeated after a random interval rand(cy)
. If the evaluated time ≤ clk.time, the event is scheduled at clk.time
.
Arguments
clk<:AbstractClock
: clock, it not supplied, the event is scheduled to 𝐶,ex<:Action
: an expression or function or a tuple of them,T::Timing
: a timing, one ofat
,after
orevery
,t
: event time,Number
orDistribution
,cy
: repeat cycle,Number
orDistribution
.
Keyword arguments
n::Int=typemax(Int)
: number of repeating events,cid::Int=clk.id
: if cid ≠ clk.id, assign the event to the parallel clock with id == cid. This overridesspawn
,spawn::Bool=false
: if true, spawn the event at other available threads,
Examples
julia> using DiscreteEvents, Distributions, Random
julia> Random.seed!(123);
julia> c = Clock()
Clock 1: state=:idle, t=0.0, Δt=0.01, prc:0
scheduled ev:0, cev:0, sampl:0
julia> f(x) = x[1] += 1
f (generic function with 1 method)
julia> a = [0]
1-element Array{Int64,1}:
0
julia> event!(c, fun(f, a), 1) # 1st event at 1
julia> event!(c, fun(f, a), at, 2) # 2nd event at 2
julia> event!(c, fun(f, a), after, 3) # 3rd event after 3
julia> event!(c, fun(f, a), every, Exponential(3)) # Poisson process with λ=1/3
julia> run!(c, 50)
"run! finished with 26 clock events, 0 sample steps, simulation time: 50.0"
julia> a
1-element Array{Int64,1}:
26
Conditional Events
Actions can be scheduled as events under given conditions:
DiscreteEvents.event!
— Methodevent!([clk], ex, cond; <keyword arguments>)
Schedule ex
as a conditional event, conditions cond
get evaluated at each clock tick.
Arguments
clk<:AbstractClock
: if no clock is supplied, the event is scheduled to 𝐶,ex<:Action
: an expression or function or a tuple of them,cond<:Action
: a condition is true if all functions or expressions therein return true,
Keyword arguments
cid::Int=clk.id
: if cid ≠ clk.id, assign the event to the parallel clock with id == cid. This overridesspawn
,spawn::Bool=false
: if true, spawn the event at other available threads.
Examples
julia> using DiscreteEvents
julia> c = Clock() # create a new clock
Clock 1: state=:idle, t=0.0, Δt=0.01, prc:0
scheduled ev:0, cev:0, sampl:0
julia> event!(c, fun((x)->println(tau(x), ": now I'm triggered"), c), fun(>=, fun(tau, c), 5))
julia> run!(c, 10) # sampling is not exact, so it takes 502 sample steps to fire the event
5.009999999999938: now I'm triggered
"run! finished with 0 clock events, 502 sample steps, simulation time: 10.0"
Conditions should be expressed with inequalities like <, ≤, ≥, > rather than with equality == in order to make sure that they can be detected, e.g. tau() ≥ 100
is preferable to tau() == 100
.
The @event Macro
If a function has a clock as its first argument, you can use the @event
macro to schedule it. This wraps it into a fun
closure and then calls event!
with it.
DiscreteEvents.@event
— Macro@event f(arg...) T t [n]
Schedule a function f(arg...)
as an event to a clock.
Arguments
f
: function to be executed at event time,arg...
: its arguments, the first argument must be a clock,T
: aTiming
(at, after, every),t
: aNumber
or aDistribution
,n::Int
: repetitions for a repeat event.
@event f(farg...) c(carg...)
@event f(farg...) ca
Schedule a function f(farg...)
as a conditional event to a clock.
Arguments
f
: function to be executed at event time,farg...
: its arguments, the first argument must be a clock,c
: function to be evaluated at the clock's sample rate, if it returns true, the event is triggered,carg...
: arguments to c,ca
: an anyonymous function of the form()->...
to be evaluated at the clock's sample rate, if it returns true, the event is triggered.
Use Cases
Currently @event
supports the following use cases (it accepts a number or a distribution as time t
argument):
@event f(clk, a, b) at t # schedule f(clk, a, b) at time t
@event f(clk, a, b) t # the same
@event f(clk, a, b) after t # schedule f(clk, a, b) after t time units
@event f(clk, a, b) every t # schedule it every t unit
@event f(clk, a, b) every t 10 # schedule it every t unit for 10 times
@event f(clk, a, b) g(c) # schedule it on condition g(c)
@event f(clk, a, b) :a ≥ 5 # schedule it on condition a ≥ 5
@event f(clk, a, b) ()-> a≥5 && tau(clk)≥8 # on condition of an anonymous function
The first call gets expanded to event!(clk, fun(f, clk, a, b), after, t)
.
Note: the @event
macro doesn't accept keyword arguments. If you want to use event!
with keyword arguments, you must use it explicitly.
Continuous Sampling
Actions can be registered for sampling and are then executed "continuously" at each clock increment Δt. The default clock sample rate Δt is 0.01 time units.
DiscreteEvents.sample_time!
— Functionsample_time!([clk::Clock], Δt::N) where {N<:Number}
Set the clock's sample rate starting from now (tau(clk)
).
Arguments
clk::Clock
: if not supplied, set the sample rate on 𝐶,Δt::N
: sample rate, time interval for sampling
DiscreteEvents.periodic!
— Functionperiodic!([clk], ex, Δt; <keyword arguments>)
periodic!(clk, ex; <keyword arguments>)
Register an Action ex
for periodic execution at the clock`s sample rate.
Arguments
clk<:AbstractClock
: if not supplied, it registers on 𝐶,ex<:Action
: an expression or function or a tuple of them,Δt<:Number=clk.Δt
: set the clock's sampling rate, if no Δt is given, it takes the current sampling rate, if ≤ 0, it calculates a positive one,
Keyword arguments
cid::Int=clk.id
: if cid ≠ clk.id, sample at the parallel clock with id == cid. This overridesspawn
,spawn::Bool=false
: if true, spawn the periodic event to other available threads.
DiscreteEvents.@periodic
— Macro@periodic f(arg...) [Δt]
Register a function f(arg...)
for periodic execution at the clock`s sample rate.
Arguments
f
: function to be executed periodically,arg...
: its arguments, the first argument must be a clock,Δt
: for setting the clock's sample rate.
Events and Variables
Actions often depend on data or modify it. The data may change between the definition of an action and its later execution. If an action uses a mutable variable like an array or a mutable struct, it gets current data at event time and it is fast. If the action modifies the data, this is the best way to do it:
julia> using DiscreteEvents
julia> a = [1] # define a mutable variable a
1-element Vector{Int64}:
1
julia> f(x; y=2) = (x[1] += y) # define a function f
f (generic function with 1 method)
julia> ff = fun(f, a); # enclose f and a in a fun ff
julia> a[1] += 1 # modify a
2
julia> ff() # execute ff
4
julia> a[1] # a has been modified correctly
4
There are good reasons to avoid global variables but if you want to work with them, you can do it in several ways:
julia> g(x; y=1) = x+y # define a function g
g (generic function with 1 method)
julia> x = 1; # define a global variable x = 1
julia> gg = fun(g, :x, y=2); # pass x as a symbol to g
julia> x += 1 # increment x
2
julia> 2
2
julia> gg() # now g gets a current x and gives a warning
┌ Warning: Evaluating expressions is slow, use functions instead
└ @ DiscreteEvents ~/work/DiscreteEvents.jl/DiscreteEvents.jl/src/fclosure.jl:28
4
julia> hh = fun(g, fun(()->x), y=3); # reference x with an anonymous fun
julia> x += 1 # increment x
3
julia> hh() # g gets again a current x
6
julia> ii = fun(g, ()->x, y=4); # reference x with an anonymous function
julia> x += 1 # increment x
4
julia> ii() # g gets an updated x
8
To modify a global variable, you have to use the global
keyword inside your function.
Events with Time Units
Timed events can be scheduled with time units. Times are converted to the clock's time unit.
julia> using Unitful
julia> import Unitful: s, minute, hr
julia> c = Clock()
Clock 1: state=:idle, t=0.0, Δt=0.01, prc:0
scheduled ev:0, cev:0, sampl:0
julia> event!(c, fun(f, a), 1s)
Warning: clock has no time unit, ignoring units
julia> setUnit!(c, s)
0.0 s
julia> event!(c, fun(f, a), 1minute)
julia> event!(c, fun(f, a), after, 1hr)