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.ActionType
Action

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.funFunction
fun(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 fs later execution. If f needs their current values at execution time there are two possibilities:

  1. fun can take funs, 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: if f is an event!, its arguments are passed on unevaluated.
  2. A mutable type argument (Array, struct ...) is always current. You can also change its content from within a function.

If using Symbols 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. Exprs and Symbols 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.

Evaluating expressions is slow!

Usage of Expr or Symbol will generate a one time warning. You can replace them easily with funs or function closures.

Timed Events

Actions can be scheduled as events at given times:

DiscreteEvents.TimingType
Timing

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!Method
event!([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 of at, after or every,
  • t: event time, Number or Distribution,
  • cy: repeat cycle, Number or Distribution.

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 overrides spawn,
  • 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!Method
event!([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 overrides spawn,
  • 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"
Use inequalities to express conditions

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.@eventMacro
@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: a Timing (at, after, every),
  • t: a Number or a Distribution,
  • 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!Function
sample_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!Function
periodic!([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 overrides spawn,
  • spawn::Bool=false: if true, spawn the periodic event to other available threads.
DiscreteEvents.@periodicMacro
@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 ~/.julia/packages/DiscreteEvents/BSM1u/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)