|
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