# Fractal Growth

This model follows the process known as diffusion-limited aggregation to simulate the growth of fractals. It is a kinetic process that consists of Randomly() diffusing particles giving rise to fractal-like structures resembling those observed naturally. This examplet is based off of "Particularly Stuck" example in Complexity Explorables.

The environment is a two dimensional, continuous space world. Agents are particles that diffuse and aggregate to form fractals. Initially, there are particles of random size distributed across the space, and one static particle in the center that forms the seed for the fractal growth. As moving particles collide with the seed or any particle that previously collided with the seed, it gets stuck and contributes to the fractal. As a particle gets stuck, another one is created at a circular border around the center to feed the growth.

It is also available from the Models module as Models.fractal_growth.

using Agents, LinearAlgebra
using Random

We use the @agent macro to conveniently define a Particle agent. Each agent has a radius, representing the particle size, a boolean to define whether it is stuck and part of the fractal, and an axis around which it spins (elaborated on later). In addition, since we use the ContinuousAgent type, the @agent macro also provides each agent with fields for id, pos (its position in space) and vel (its velocity).

@agent struct Particle(ContinuousAgent{2, Float64})
is_stuck::Bool
spin_axis::Array{Float64,1}
end

propParticle(radius::Float64, spin_clockwise::Bool; is_stuck = false,) =
((0.0, 0.0), radius, is_stuck, [0.0, 0.0, spin_clockwise ? -1.0 : 1.0])
propParticle (generic function with 1 method)

We also define a few utility functions for ease of implementation. rand_circle returns a random point on the unit circle. particle_radius generates a random radius for a particle, within given range defined by min_radius and max_radius. If max_radius < min_radius, it returns min_radius: allowing a fixed particle size to be specified.

rand_circle(rng) = (θ = rand(rng, 0.0:0.1:359.9); (cos(θ), sin(θ)))
particle_radius (generic function with 1 method)

The initialize_model function returns a new model containing particles placed Randomly() in the given space and one seed particle at the center.

function initialize_model(;
initial_particles::Int = 100, # initial particles in the model, not including the seed
# size of the space in which particles exist
space_extents::NTuple{2,Float64} = (150.0, 150.0),
speed = 0.5, # speed of particle movement
vibration = 0.55, # amplitude of particle vibration
attraction = 0.45, # velocity of particles towards the center
spin = 0.55, # tangential velocity with which particles orbit the center
# fraction of particles orbiting clockwise. The rest are anticlockwise
clockwise_fraction = 0.0,
seed = 42,
)
properties = Dict(
:speed => speed,
:vibration => vibration,
:attraction => attraction,
:spin => spin,
:clockwise_fraction => clockwise_fraction,
:spawn_count => 0,
)
# space is periodic to allow particles going off one edge to wrap around to the opposite
space = ContinuousSpace(space_extents; spacing = 1.0, periodic = true)
model = ABM(Particle, space; properties, agent_step!, model_step!,
rng = MersenneTwister(seed))
center = space_extents ./ 2.0
for i in 1:initial_particles
props = propParticle(radius, rand(abmrng(model)) < clockwise_fraction)
# add_agent! automatically gives the particle a random position in the space
end
# create the seed particle
props_seed_particle = propParticle(radius, true; is_stuck = true)
# add_agent_pos! will use the position of the agent passed in, instead of assigning it
# to a random value
return model
end
initialize_model (generic function with 1 method)

The agent_step! function simulates particle motion for those who are not yet stuck. For each particle, we first perform a crude distance check to all other particles. If the current particle intersects any particle in the fractal, it also becomes part of the fractal and is not simulated further. Agent velocity has a radial component that attracts it towards the center, a tangential component that makes it orbit around the center, and a random component that simulates vibration of the particle. The velocity is scaled to be inversely proportional to the square of the particle's radius, so that larger particles move slower. The speed parameter is implemented as the time difference between successive steps of the simulation. A larger value causes particles to move more per step, but leads to inaccuracies as particles do not move through the intervening space.

function agent_step!(agent::Particle, model)
agent.is_stuck && return

for id in nearby_ids(agent.pos, model, agent.radius)
if model[id].is_stuck
agent.is_stuck = true
# increment count to make sure another particle is spawned as this one gets stuck
model.spawn_count += 1
return
end
end
# radial vector towards the center of the space
radial = abmspace(model).extent ./ 2.0 .- agent.pos
# tangential vector in the direction of orbit of the particle
agent.vel =
(
radial .* model.attraction .+ tangent .* model.spin .+
rand_circle(abmrng(model)) .* model.vibration
move_agent!(agent, model, model.speed)
end
agent_step! (generic function with 1 method)

The model_step! function serves the sole purpose of spawning additional particles as they get stuck to the growing fractal.

function model_step!(model)
while model.spawn_count > 0
props = propParticle(radius, rand(abmrng(model)) < model.clockwise_fraction)
pos = (rand_circle(abmrng(model)) .+ 1.0) .* abmspace(model).extent .* 0.49
model.spawn_count -= 1
end
end
model_step! (generic function with 1 method)

# Running the model

We run the model using the InteractiveDynamics package with GLMakie backend so the fractal growth can be visualised as it happens. InteractiveDynamics provides the abmvideo function to easily record a video of the simulation running.

model = initialize_model()

using CairoMakie

Particles that are stuck and part of the fractal are shown in red, for visual distinction

particle_color(a::Particle) = a.is_stuck ? :red : :blue
particle_color (generic function with 1 method)

The visual size of particles corresponds to their radius, and has been calculated for the default value of space_extents of the initialize_model function. It will not look accurate on other values.

abmvideo(
"fractal.mp4",
model;
agent_color = particle_color,
agent_size = particle_size,
agent_marker = '●',
spf = 20,
frames = 60,
framerate = 25,
title = "Fractal Growth",
agentsplotkwargs = (strokewidth = 0.5, strokecolor = :white),
)
┌ Warning: keyword spf is deprecated in favor of dt.
└ @ AgentsVisualizations ~/.julia/packages/Agents/oU8Ee/ext/AgentsVisualizations/src/convenience.jl:102