[ANN] MiniEvents.jl - efficient continuos-time, discrete-events agent-based models

MiniEvents.jl allows for the easy definition and efficient execution of continuous-time, discrete-events agent-based models (ABMs). Usually ABMs are defined in terms of discrete time steps, where each agent performs a number of actions at every time step. For efficiency and epistemological reasons it can be preferable, however, to define the model as a number of events that occur stochastically in continuous time at given rates.

Using MiniEvents such a model can look like this:

struct World
    pop :: Vector{Person}
end

@kwdef struct Params
	r_inf :: Float64 = 1e-6
	r_rec :: Float64 = 1e-2
	t_inf :: Float64 = 1.0
end

# events that can apply to a Person
@events person::Person begin
    # this is optional, it checks if activated objects have changed state since the last activation
    @debug

    @rate(count(p->p.status == infected, person.contacts) * @sim().pars.r_inf) ~
        # this is a boolean condition that determines whether the event can take place
        person.status == susceptible =>
        begin
            infect!(person)
            # all objects whose event rates are affected need to be refreshed
            @r person, person.contacts
        end

    @rate(@sim().pars.r_rec) ~
        person.status == infected =>
        begin
            heal!(person)
            @r person person.contacts
        end
end

# events that can apply to the World
@events world::World begin
	@debug

	# events with a fixed time are supported as well
	@repeat(@sim().pars.t_inf) =>
	begin
		p = rand(world.pop)
		p.status = infected
		@r p
	end
end

# @simulation ties everything together
@simulation Model Person World begin
	# we can add custom properties 
	world :: World
	pars :: Params
end

function MiniEvents.spawn!(model::Model)
    # before an object can receive events it needs to be activated
    # spawn_pop! activates a list of objects at once
    spawn_pop!(model.world.pop, model)
    spawn!(model.world, model)
end

function step!(model)
    # find and execute the next event in line
    next_event!(model)
end

MiniEvents implements the direct method of the Gillespie algorithm and avoids memory allocations and dynamic function calls. Execution speed depends on model size (due to memory access) and model complexity, but even with a quarter of a million agents, hundreds of thousands of events per second can be processed (time for 500k events show):

.6086.0
grid size: 2
  48.109 ms (0 allocations: 0 bytes)
grid size: 4
  72.159 ms (0 allocations: 0 bytes)
grid size: 8
  95.049 ms (0 allocations: 0 bytes)
grid size: 16
  114.925 ms (0 allocations: 0 bytes)
grid size: 32
  141.664 ms (0 allocations: 0 bytes)
grid size: 64
  175.284 ms (0 allocations: 0 bytes)
grid size: 128
  228.154 ms (0 allocations: 0 bytes)
grid size: 256
  327.094 ms (0 allocations: 0 bytes)
grid size: 512
  415.060 ms (0 allocations: 0 bytes)
3 Likes