|
2 | 2 | EditURL = "https://github.com/JuliaGeometry/DelaunayTriangulation.jl/tree/main/docs/src/literate_applications/cell_simulations.jl" |
3 | 3 | ``` |
4 | 4 |
|
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 | | - |
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](). |
379 | 6 |
|
380 | 7 | *This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* |
381 | 8 |
|
0 commit comments