Skip to content

Commit ec3059e

Browse files
authored
Move cell example to Agents.jl (#203)
1 parent 7e2f75c commit ec3059e

File tree

2 files changed

+3
-574
lines changed

2 files changed

+3
-574
lines changed

docs/src/applications/cell_simulations.md

+1-374
Original file line numberDiff line numberDiff line change
@@ -2,380 +2,7 @@
22
EditURL = "https://github.com/JuliaGeometry/DelaunayTriangulation.jl/tree/main/docs/src/literate_applications/cell_simulations.jl"
33
```
44

5-
# Cellular Biology
6-
Our next application concerns the simulation of cell dynamics in two dimensions.[^1] We only consider a very basic
7-
model here, allowing only diffusion and proliferation. The point here is to just demonstrate how we can make use of the neighbourhood graph
8-
in a `Triangulation` to perform this type of simulation. A good paper discussing these types of simulations is the paper
9-
[_Comparing individual-based approaches to modelling the self-organization of multicellular tissues_](https://doi.org/10.1371/journal.pcbi.1005387) by Osborne et al. (2017).
10-
11-
[^1]: If you are interested in how these ideas are applied in one dimension, see also [EpithelialDynamics1D.jl](https://github.com/DanielVandH/EpithelialDynamics1D.jl).
12-
13-
## Cell model
14-
Let us consider a domain $\Omega$, and suppose we have an initial
15-
set of points $\mathcal P = \{\vb r_1, \ldots, \vb r_N\}$ in the plane. We use a spring based model based on Hooke's law and Newton's laws,
16-
writing
17-
18-
```math
19-
\dv{\vb r_i}{t} = \alpha \sum_{j \in \mathcal N_i} \left(\|\vb r_{ji}\| - s\right)\hat{\vb r}_{ji}, \quad i=1,2,\ldots,N,
20-
```
21-
where $\mathcal N_i$ is the set of points neighbouring $\vb r_i$ in the triangulation $\mathcal D\mathcal T(\mathcal P)$,
22-
$\vb r_{ji} = \vb r_j - \vb r_i$, $\hat{\vb r}_{ji} = \vb r_{ji} / \|\vb r_{ji}\|$, $s$ is the resting spring length,
23-
and $\alpha$ is the ratio of the spring constant $k$ to the damping constant $\eta$.
24-
We will allow the boundary nodes to move freely rather than consider some types of boundary conditions.
25-
26-
Now consider proliferation. We interpret the cells as being the Voronoi polygons associated with each $\vb r_i$, i.e. the cell associated
27-
with $\vb r_i$ is $\mathcal V_i$. We allow only one
28-
cell to proliferate over a time interval $[t, t + \Delta t)$ and, given that a cell does proliferate, the probability that $\mathcal V_i$ proliferates
29-
is $G_i\Delta t$ with $G_i = \max\{0, \beta(1 - 1/(KA_i))\}$, where $\beta$ is the intrinsic proliferation rate, $K$ is the cell carrying
30-
capacity density, and $A_i$ is the area of $\mathcal V_i$. For cells on the boundary, $G_i = 0$ so that they do not proliferate; this is done
31-
to avoid issues with unbounded cells. The probability any proliferation event occurs is given by $\sum_i G_i\Delta t$. When a cell $\mathcal V_i$
32-
is selected to proliferate, we select a random angle $\theta \in [0, 2\pi)$ and place a new point $\vb r_i'$ at $\vb r_i' = \vb r_i + \delta \epsilon \vb d$,
33-
where $\vb d = (\cos\theta,\sin\theta)$, $\delta = \operatorname{dist}(\vb r_i, \mathcal V_i)$, and $\epsilon$ is a small separation constant in $[0, 1)$.
34-
35-
For our simulation, we will first determine if any proliferation event occurs and, if so, update the triangulation with the new $\vb r_i'$. To then
36-
integrate forward in time, we use Euler's method, writing
37-
38-
```math
39-
\vb r_i(t + \Delta t) = \vb r_i(t) + \alpha \Delta t \sum_{j \in \mathcal N_i} \left(\|\vb r_{ji}\| - s\right)\hat{\vb r}_{ji}, \quad i=1,2,\ldots,N.
40-
```
41-
42-
We update the triangulation after each step.[^2]
43-
44-
[^2]: There are efficient methods for updating the triangulation after small perturbations, e.g. the paper [_Star splaying: an algorithm for repairing Delaunay triangulations and convex hulls_](https://doi.org/10.1145/1064092.1064129) by Shewchuk (2005). For this implementation, we do not concern ourselves with being overly efficient, and simply retriangulate.
45-
46-
## Implementation
47-
Let us now implement this model. First, we define a struct for storing the parameters of our model.
48-
49-
````@example cell_simulations
50-
using DelaunayTriangulation
51-
using StableRNGs
52-
using LinearAlgebra
53-
using StatsBase
54-
using CairoMakie
55-
Base.@kwdef mutable struct CellModel{P}
56-
tri::Triangulation{P}
57-
new_r_cache::Vector{NTuple{2, Float64}} # for r(t + Δt)
58-
α::Float64
59-
s::Float64
60-
Δt::Float64
61-
rng::StableRNGs.LehmerRNG
62-
final_time::Float64
63-
β::Float64
64-
K::Float64
65-
ϵ::Float64
66-
end
67-
````
68-
69-
Let's now write functions for performing the migration step.
70-
71-
````@example cell_simulations
72-
function migrate_cells!(cells::CellModel) # a more efficient way would be to loop over edges rather than vertices
73-
tri = cells.tri
74-
for i in each_solid_vertex(tri)
75-
rᵢ = get_point(tri, i)
76-
F = 0.0
77-
for j in get_neighbours(tri, i)
78-
DelaunayTriangulation.is_ghost_vertex(j) && continue
79-
rⱼ = get_point(tri, j)
80-
rᵢⱼ = rⱼ .- rᵢ
81-
F = F .+ cells.α * (norm(rᵢⱼ) - cells.s) .* rᵢⱼ ./ norm(rᵢⱼ)
82-
end
83-
cells.new_r_cache[i] = rᵢ .+ cells.Δt .* F
84-
end
85-
for i in each_solid_vertex(tri)
86-
DelaunayTriangulation.set_point!(tri, i, cells.new_r_cache[i])
87-
end
88-
cells.tri = retriangulate(tri; cells.rng)
89-
return nothing
90-
end
91-
````
92-
93-
Now we can write the proliferation functions. First, let us write a function that computes the Voronoi areas. If we had the
94-
`VoronoiTessellation` computed, we would just use `get_area`, but we are aiming to avoid having to compute $\mathcal V\mathcal T(\mathcal P)$
95-
directly.
96-
97-
````@example cell_simulations
98-
function polygon_area(points) # this is the same function from the Interpolation section
99-
n = DelaunayTriangulation.num_points(points)
100-
p, q, r, s = get_point(points, 1, 2, n, n - 1)
101-
px, py = getxy(p)
102-
qx, qy = getxy(q)
103-
rx, ry = getxy(r)
104-
sx, sy = getxy(s)
105-
area = px * (qy - ry) + rx * (py - sy)
106-
for i in 2:(n - 1)
107-
p, q, r = get_point(points, i, i + 1, i - 1)
108-
px, py = getxy(p)
109-
qx, qy = getxy(q)
110-
rx, ry = getxy(r)
111-
area += px * (qy - ry)
112-
end
113-
return area / 2
114-
end
115-
function get_voronoi_area(tri::Triangulation, i)
116-
points = NTuple{2, Float64}[]
117-
!DelaunayTriangulation.has_vertex(tri, i) && return (0.0, points) # might not be included anymore due to retriangulation
118-
DelaunayTriangulation.is_boundary_node(tri, i)[1] && return (0.0, points) # to prevent boundary cells from proliferating
119-
N = get_neighbours(tri, i)
120-
N₁ = first(N)
121-
j = N₁
122-
k = get_adjacent(tri, i, j)
123-
Ttype = DelaunayTriangulation.triangle_type(tri)
124-
for _ in 1:num_neighbours(tri, i)
125-
T = DelaunayTriangulation.construct_triangle(Ttype, i, j, k)
126-
c = DelaunayTriangulation.triangle_circumcenter(tri, T)
127-
push!(points, c)
128-
j = k
129-
k = get_adjacent(tri, i, j)
130-
end
131-
push!(points, points[begin])
132-
return polygon_area(points), points
133-
end
134-
````
135-
136-
The function `get_voronoi_area` above returns `0` if the cell is on the boundary. Finally, our function for performing the
137-
proliferation step is below.
138-
139-
````@example cell_simulations
140-
function proliferate_cells!(cells::CellModel)
141-
E = 0.0
142-
Δt = cells.Δt
143-
tri = cells.tri
144-
areas = [get_voronoi_area(tri, i)[1] for i in DelaunayTriangulation.each_point_index(tri)] # don't use each_solid_vertex since each_solid_vertex is not ordered
145-
for i in DelaunayTriangulation.each_point_index(tri)
146-
Gᵢ = iszero(areas[i]) ? 0.0 : max(0.0, cells.β * (1 - 1 / (cells.K * areas[i])))
147-
E += Gᵢ * Δt
148-
end
149-
u = rand(cells.rng)
150-
if u < E
151-
areas ./= sum(areas)
152-
weights = Weights(areas)
153-
i = sample(cells.rng, weights) # select cell to proliferate randomly based on area
154-
θ = 2π * rand(cells.rng)
155-
p = get_point(tri, i)
156-
poly = get_voronoi_area(tri, i)[2]
157-
δ = DelaunayTriangulation.distance_to_polygon(p, get_points(tri), poly)
158-
s, c = sincos(θ)
159-
q = p .+ (δ * cells.ϵ) .* (c, s)
160-
add_point!(tri, q; rng = cells.rng)
161-
push!(cells.new_r_cache, q)
162-
end
163-
return nothing
164-
end
165-
````
166-
167-
Finally, our simulation function is below.
168-
169-
````@example cell_simulations
170-
function perform_step!(cells::CellModel)
171-
proliferate_cells!(cells)
172-
migrate_cells!(cells)
173-
return nothing
174-
end
175-
function simulate_cells(cells::CellModel)
176-
t = 0.0
177-
all_points = Vector{Vector{NTuple{2, Float64}}}()
178-
push!(all_points, deepcopy(get_points(cells.tri)))
179-
while t < cells.final_time
180-
perform_step!(cells)
181-
t += cells.Δt
182-
push!(all_points, deepcopy(get_points(cells.tri)))
183-
end
184-
return all_points
185-
end
186-
````
187-
188-
## Example
189-
Let us now give an example. Our initial set of points will be randomly chosen inside
190-
the rectangle $[-2, 2] \times [-5, 5]$. We use $\alpha = 5$, $s = 2$, $\Delta t = 10^{-3}$,
191-
$\beta = 0.25$, $K = 100^2$, and $\epsilon = 0.5$.
192-
193-
````@example cell_simulations
194-
rng = StableRNG(123444)
195-
a, b, c, d = -2.0, 2.0, -5.0, 5.0
196-
points = [(a + (b - a) * rand(rng), c + (d - c) * rand(rng)) for _ in 1:10]
197-
tri = triangulate(points; rng = rng)
198-
cells = CellModel(;
199-
tri = tri, new_r_cache = similar(points), α = 5.0, s = 2.0, Δt = 1.0e-3,
200-
β = 0.25, K = 100.0^2, rng, final_time = 25.0, ϵ = 0.5,
201-
)
202-
results = simulate_cells(cells);
203-
204-
fig = Figure(fontsize = 26)
205-
title_obs = Observable(L"t = %$(0.0)")
206-
ax1 = Axis(fig[1, 1], width = 1200, height = 400, title = title_obs, titlealign = :left)
207-
Δt = cells.Δt
208-
i = Observable(1)
209-
voronoiplot!(
210-
ax1, @lift(voronoi(triangulate(results[$i]; rng), clip = true, rng = rng)),
211-
color = :darkgreen, strokecolor = :black, strokewidth = 2, show_generators = false,
212-
)
213-
xlims!(ax1, -12, 12)
214-
ylims!(ax1, -12, 12)
215-
resize_to_layout!(fig)
216-
t = 0:Δt:cells.final_time
217-
record(fig, "cell_simulation.mp4", 1:10:length(t); framerate = 60) do ii
218-
i[] = ii
219-
title_obs[] = L"t = %$(((ii-1) * Δt))"
220-
end;
221-
nothing #hide
222-
````
223-
224-
![](cell_simulation.mp4)
225-
226-
## Just the code
227-
An uncommented version of this example is given below.
228-
You can view the source code for this file [here](https://github.com/JuliaGeometry/DelaunayTriangulation.jl/tree/main/docs/src/literate_applications/cell_simulations.jl).
229-
230-
```julia
231-
using DelaunayTriangulation
232-
using StableRNGs
233-
using LinearAlgebra
234-
using StatsBase
235-
using CairoMakie
236-
Base.@kwdef mutable struct CellModel{P}
237-
tri::Triangulation{P}
238-
new_r_cache::Vector{NTuple{2, Float64}} # for r(t + Δt)
239-
α::Float64
240-
s::Float64
241-
Δt::Float64
242-
rng::StableRNGs.LehmerRNG
243-
final_time::Float64
244-
β::Float64
245-
K::Float64
246-
ϵ::Float64
247-
end
248-
249-
function migrate_cells!(cells::CellModel) # a more efficient way would be to loop over edges rather than vertices
250-
tri = cells.tri
251-
for i in each_solid_vertex(tri)
252-
rᵢ = get_point(tri, i)
253-
F = 0.0
254-
for j in get_neighbours(tri, i)
255-
DelaunayTriangulation.is_ghost_vertex(j) && continue
256-
rⱼ = get_point(tri, j)
257-
rᵢⱼ = rⱼ .- rᵢ
258-
F = F .+ cells.α * (norm(rᵢⱼ) - cells.s) .* rᵢⱼ ./ norm(rᵢⱼ)
259-
end
260-
cells.new_r_cache[i] = rᵢ .+ cells.Δt .* F
261-
end
262-
for i in each_solid_vertex(tri)
263-
DelaunayTriangulation.set_point!(tri, i, cells.new_r_cache[i])
264-
end
265-
cells.tri = retriangulate(tri; cells.rng)
266-
return nothing
267-
end
268-
269-
function polygon_area(points) # this is the same function from the Interpolation section
270-
n = DelaunayTriangulation.num_points(points)
271-
p, q, r, s = get_point(points, 1, 2, n, n - 1)
272-
px, py = getxy(p)
273-
qx, qy = getxy(q)
274-
rx, ry = getxy(r)
275-
sx, sy = getxy(s)
276-
area = px * (qy - ry) + rx * (py - sy)
277-
for i in 2:(n - 1)
278-
p, q, r = get_point(points, i, i + 1, i - 1)
279-
px, py = getxy(p)
280-
qx, qy = getxy(q)
281-
rx, ry = getxy(r)
282-
area += px * (qy - ry)
283-
end
284-
return area / 2
285-
end
286-
function get_voronoi_area(tri::Triangulation, i)
287-
points = NTuple{2, Float64}[]
288-
!DelaunayTriangulation.has_vertex(tri, i) && return (0.0, points) # might not be included anymore due to retriangulation
289-
DelaunayTriangulation.is_boundary_node(tri, i)[1] && return (0.0, points) # to prevent boundary cells from proliferating
290-
N = get_neighbours(tri, i)
291-
N₁ = first(N)
292-
j = N₁
293-
k = get_adjacent(tri, i, j)
294-
Ttype = DelaunayTriangulation.triangle_type(tri)
295-
for _ in 1:num_neighbours(tri, i)
296-
T = DelaunayTriangulation.construct_triangle(Ttype, i, j, k)
297-
c = DelaunayTriangulation.triangle_circumcenter(tri, T)
298-
push!(points, c)
299-
j = k
300-
k = get_adjacent(tri, i, j)
301-
end
302-
push!(points, points[begin])
303-
return polygon_area(points), points
304-
end
305-
306-
function proliferate_cells!(cells::CellModel)
307-
E = 0.0
308-
Δt = cells.Δt
309-
tri = cells.tri
310-
areas = [get_voronoi_area(tri, i)[1] for i in DelaunayTriangulation.each_point_index(tri)] # don't use each_solid_vertex since each_solid_vertex is not ordered
311-
for i in DelaunayTriangulation.each_point_index(tri)
312-
Gᵢ = iszero(areas[i]) ? 0.0 : max(0.0, cells.β * (1 - 1 / (cells.K * areas[i])))
313-
E += Gᵢ * Δt
314-
end
315-
u = rand(cells.rng)
316-
if u < E
317-
areas ./= sum(areas)
318-
weights = Weights(areas)
319-
i = sample(cells.rng, weights) # select cell to proliferate randomly based on area
320-
θ = 2π * rand(cells.rng)
321-
p = get_point(tri, i)
322-
poly = get_voronoi_area(tri, i)[2]
323-
δ = DelaunayTriangulation.distance_to_polygon(p, get_points(tri), poly)
324-
s, c = sincos(θ)
325-
q = p .+* cells.ϵ) .* (c, s)
326-
add_point!(tri, q; rng = cells.rng)
327-
push!(cells.new_r_cache, q)
328-
end
329-
return nothing
330-
end
331-
332-
function perform_step!(cells::CellModel)
333-
proliferate_cells!(cells)
334-
migrate_cells!(cells)
335-
return nothing
336-
end
337-
function simulate_cells(cells::CellModel)
338-
t = 0.0
339-
all_points = Vector{Vector{NTuple{2, Float64}}}()
340-
push!(all_points, deepcopy(get_points(cells.tri)))
341-
while t < cells.final_time
342-
perform_step!(cells)
343-
t += cells.Δt
344-
push!(all_points, deepcopy(get_points(cells.tri)))
345-
end
346-
return all_points
347-
end
348-
349-
rng = StableRNG(123444)
350-
a, b, c, d = -2.0, 2.0, -5.0, 5.0
351-
points = [(a + (b - a) * rand(rng), c + (d - c) * rand(rng)) for _ in 1:10]
352-
tri = triangulate(points; rng = rng)
353-
cells = CellModel(;
354-
tri = tri, new_r_cache = similar(points), α = 5.0, s = 2.0, Δt = 1.0e-3,
355-
β = 0.25, K = 100.0^2, rng, final_time = 25.0, ϵ = 0.5,
356-
)
357-
results = simulate_cells(cells);
358-
359-
fig = Figure(fontsize = 26)
360-
title_obs = Observable(L"t = %$(0.0)")
361-
ax1 = Axis(fig[1, 1], width = 1200, height = 400, title = title_obs, titlealign = :left)
362-
Δt = cells.Δt
363-
i = Observable(1)
364-
voronoiplot!(
365-
ax1, @lift(voronoi(triangulate(results[$i]; rng), clip = true, rng = rng)),
366-
color = :darkgreen, strokecolor = :black, strokewidth = 2, show_generators = false,
367-
)
368-
xlims!(ax1, -12, 12)
369-
ylims!(ax1, -12, 12)
370-
resize_to_layout!(fig)
371-
t = 0:Δt:cells.final_time
372-
record(fig, "cell_simulation.mp4", 1:10:length(t); framerate = 60) do ii
373-
i[] = ii
374-
title_obs[] = L"t = %$(((ii-1) * Δt))"
375-
end;
376-
```
377-
378-
---
5+
This example has been moved to Agent.jl's documentation. Please see the example [here]().
3796

3807
*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*
3818

0 commit comments

Comments
 (0)