# 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`

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 Particle ContinuousAgent{2} begin
radius::Float64
is_stuck::Bool
spin_axis::Array{Float64,1}
end
```

A custom constructor allows convenient creation of agents.

```
Particle(
id::Int,
radius::Float64,
spin_clockwise::Bool;
pos = (0.0, 0.0),
is_stuck = false,
) = Particle(id, pos, (0.0, 0.0), radius, is_stuck, [0.0, 0.0, spin_clockwise ? -1.0 : 1.0])
```

`Main.Particle`

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(min_radius::Float64, max_radius::Float64, rng) =
min_radius <= max_radius ? rand(rng, min_radius:0.01:max_radius) : min_radius
```

`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,
min_radius = 1.0, # minimum radius of any particle
max_radius = 2.0, # maximum radius of any particle
)
properties = Dict(
:speed => speed,
:vibration => vibration,
:attraction => attraction,
:spin => spin,
:clockwise_fraction => clockwise_fraction,
:min_radius => min_radius,
:max_radius => max_radius,
:spawn_count => 0,
)
# space is periodic to allow particles going off one edge to wrap around to the opposite
space = ContinuousSpace(space_extents, 1.0; periodic = true)
model = ABM(Particle, space; properties)
center = space_extents ./ 2.0
for i in 1:initial_particles
particle = Particle(
i,
particle_radius(min_radius, max_radius, model.rng),
rand(model.rng) < clockwise_fraction,
)
# `add_agent!` automatically gives the particle a random position in the space
add_agent!(particle, model)
end
# create the seed particle
particle = Particle(
initial_particles + 1,
particle_radius(min_radius, max_radius, model.rng),
true;
pos = center,
is_stuck = true,
)
# `add_agent_pos!` will use the position of the agent passed in, instead of assigning it
# to a random value
add_agent_pos!(particle, model)
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 = model.space.extent ./ 2.0 .- agent.pos
radial = radial ./ norm(radial)
# tangential vector in the direction of orbit of the particle
tangent = Tuple(cross([radial..., 0.0], agent.spin_axis)[1:2])
agent.vel =
(
radial .* model.attraction .+ tangent .* model.spin .+
rand_circle(model.rng) .* model.vibration
) ./ (agent.radius^2.0)
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
particle = Particle(
nextid(model),
particle_radius(model.min_radius, model.max_radius, model.rng),
rand(model.rng) < model.clockwise_fraction;
pos = (rand_circle(model.rng) .+ 1.0) .* model.space.extent .* 0.49,
)
add_agent_pos!(particle, model)
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 `abm_video`

function to easily record a video of the simulation running.

```
model = initialize_model()
using InteractiveDynamics
import 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
# 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.
particle_size(a::Particle) = 7.5 * a.radius
abm_video(
"fractal.mp4",
model,
agent_step!,
model_step!;
ac = particle_color,
as = particle_size,
am = '●',
spf = 10,
frames = 600,
framerate = 25,
scatterkwargs = (strokewidth = 0.5, strokecolor = :white),
)
```

Using `InteractiveDynamics`

simulation parameters can also be tweaked dynamically. This makes use of the `InteractiveDynamics.abm_data_exploration`

function.

```
using InteractiveDynamics
using GLMakie
model = initialize_model()
```

`params`

defines the range in which different parameter values can be adjusted through sliders.

```
params = (
:attraction => 0.0:0.01:2.0,
:speed => 0.0:0.01:2.0,
:vibration => 0.0:0.01:2.0,
:spin => 0.0:0.01:2.0,
:clockwise_fraction => 0.0:0.01:1.0,
:min_radius => 0.5:0.01:3.0,
:max_radius => 0.5:0.01:3.0,
)
particle_size(a::Particle) = 4 * a.radius
abm_data_exploration(
model,
agent_step!,
model_step!,
params;
ac = particle_color,
as = particle_size,
am = '⚪',
)
```