1use crate::lattice::Lattice;
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct Species {
17 pub name: String,
19 pub diffusion: f64,
21 pub decay: f64,
23 pub initial: f64,
25}
26
27#[derive(Clone, Debug, Default, PartialEq)]
29pub struct Exchange {
30 pub secretion: Vec<f64>,
32 pub uptake: Vec<f64>,
34}
35
36#[derive(Clone, Debug)]
38pub struct Fields {
39 pub species: Vec<Species>,
41 pub values: Vec<Vec<f64>>,
43 pub substeps: Vec<usize>,
45 scratch: Vec<f64>,
46}
47
48#[must_use]
50pub fn stability_limit(dimensions: usize) -> f64 {
51 1.0 / (2.0 * dimensions.max(1) as f64)
52}
53
54impl Fields {
55 #[must_use]
57 pub fn new(species: Vec<Species>, sites: usize, dimensions: usize) -> Self {
58 let values = species
59 .iter()
60 .map(|s| vec![s.initial; sites])
61 .collect::<Vec<_>>();
62 let substeps = species
63 .iter()
64 .map(|s| substeps_for(s.diffusion, dimensions))
65 .collect();
66 Self {
67 species,
68 values,
69 substeps,
70 scratch: vec![0.0; sites],
71 }
72 }
73
74 #[must_use]
76 pub fn len(&self) -> usize {
77 self.species.len()
78 }
79
80 #[must_use]
82 pub fn is_empty(&self) -> bool {
83 self.species.is_empty()
84 }
85
86 #[must_use]
88 pub fn index_of(&self, name: &str) -> Option<usize> {
89 self.species.iter().position(|s| s.name == name)
90 }
91
92 #[must_use]
94 pub fn total(&self, index: usize) -> f64 {
95 self.values[index].iter().sum()
96 }
97
98 pub fn diffuse(&mut self, lattice: &Lattice) {
100 for index in 0..self.species.len() {
101 let steps = self.substeps[index];
102 let d = self.species[index].diffusion / steps as f64;
103 let decay = self.species[index].decay / steps as f64;
104 for _ in 0..steps {
105 self.one_substep(lattice, index, d, decay);
106 }
107 }
108 }
109
110 fn one_substep(&mut self, lattice: &Lattice, index: usize, d: f64, decay: f64) {
111 let faces: &[(i64, i64, i64)] = if lattice.depth > 1 {
114 &crate::lattice::ORDER1
115 } else {
116 &crate::lattice::ORDER1[..4]
117 };
118 {
119 let values = &self.values[index];
120 for here in 0..values.len() {
121 let (x, y, z) = lattice.coords(here);
122 let mut laplacian = -(faces.len() as f64) * values[here];
123 for &(dx, dy, dz) in faces {
124 laplacian += values[lattice.index(x + dx, y + dy, z + dz)];
125 }
126 self.scratch[here] = values[here] + d * laplacian - decay * values[here];
127 }
128 }
129 self.values[index].copy_from_slice(&self.scratch);
130 }
131
132 pub fn exchange(&mut self, lattice: &Lattice, cell_type: &[u8], exchange: &[Exchange]) {
134 for (index, values) in self.values.iter_mut().enumerate() {
135 for (site, &label) in lattice.labels.iter().enumerate() {
136 if label == 0 {
137 continue;
138 }
139 let spec = &exchange[cell_type[label as usize] as usize];
140 let secreted = spec.secretion.get(index).copied().unwrap_or(0.0);
141 let taken = spec.uptake.get(index).copied().unwrap_or(0.0);
142 values[site] += secreted;
143 values[site] -= taken * values[site];
144 if values[site] < 0.0 {
145 values[site] = 0.0;
146 }
147 }
148 }
149 }
150}
151
152#[must_use]
155pub fn substeps_for(diffusion: f64, dimensions: usize) -> usize {
156 if diffusion <= 0.0 {
157 return 1;
158 }
159 (diffusion / stability_limit(dimensions)).ceil().max(1.0) as usize
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn flat(species: Vec<Species>, w: usize, h: usize) -> (Fields, Lattice) {
167 (Fields::new(species, w * h, 2), Lattice::medium(w, h, 1, 2))
168 }
169
170 fn cube(species: Vec<Species>, side: usize) -> (Fields, Lattice) {
171 (
172 Fields::new(species, side * side * side, 3),
173 Lattice::medium(side, side, side, 2),
174 )
175 }
176
177 #[test]
178 fn a_uniform_field_stays_uniform_under_diffusion() {
179 let (mut fields, lattice) = flat(
180 vec![Species {
181 name: "a".into(),
182 diffusion: 0.2,
183 decay: 0.0,
184 initial: 3.0,
185 }],
186 16,
187 16,
188 );
189 fields.diffuse(&lattice);
190 assert!(fields.values[0].iter().all(|v| (v - 3.0).abs() < 1e-12));
191 }
192
193 #[test]
194 fn diffusion_conserves_the_total_without_decay() {
195 let (mut fields, lattice) = flat(
196 vec![Species {
197 name: "a".into(),
198 diffusion: 0.2,
199 decay: 0.0,
200 initial: 0.0,
201 }],
202 16,
203 16,
204 );
205 fields.values[0][8 * 16 + 8] = 100.0;
206 let before = fields.total(0);
207 for _ in 0..50 {
208 fields.diffuse(&lattice);
209 }
210 assert!(
211 (fields.total(0) - before).abs() < 1e-9,
212 "{}",
213 fields.total(0)
214 );
215 }
216
217 #[test]
218 fn a_point_source_spreads_and_stays_positive() {
219 let (mut fields, lattice) = flat(
220 vec![Species {
221 name: "a".into(),
222 diffusion: 0.2,
223 decay: 0.0,
224 initial: 0.0,
225 }],
226 32,
227 32,
228 );
229 let centre = 16 * 32 + 16;
230 fields.values[0][centre] = 100.0;
231 for _ in 0..30 {
232 fields.diffuse(&lattice);
233 }
234 assert!(fields.values[0].iter().all(|&v| v >= 0.0));
235 assert!(fields.values[0][centre] < 100.0);
236 assert!(fields.values[0][centre + 3] > 0.0);
237 }
238
239 #[test]
240 fn decay_removes_a_stated_fraction() {
241 let (mut fields, lattice) = flat(
242 vec![Species {
243 name: "a".into(),
244 diffusion: 0.0,
245 decay: 0.1,
246 initial: 1.0,
247 }],
248 8,
249 8,
250 );
251 fields.diffuse(&lattice);
252 assert!((fields.values[0][0] - 0.9).abs() < 1e-12);
253 }
254
255 #[test]
256 fn a_large_diffusion_constant_takes_more_substeps() {
257 assert_eq!(substeps_for(0.0, 2), 1);
258 assert_eq!(substeps_for(0.25, 2), 1);
259 assert_eq!(substeps_for(0.26, 2), 2);
260 assert_eq!(substeps_for(2.0, 2), 8);
261 }
262
263 #[test]
264 fn a_third_dimension_tightens_the_limit() {
265 assert!((stability_limit(2) - 0.25).abs() < 1e-12);
266 assert!((stability_limit(3) - 1.0 / 6.0).abs() < 1e-12);
267 assert_eq!(substeps_for(1.0, 2), 4);
269 assert_eq!(substeps_for(1.0, 3), 6);
270 }
271
272 #[test]
273 fn diffusion_conserves_the_total_in_a_volume() {
274 let (mut fields, lattice) = cube(
275 vec![Species {
276 name: "a".into(),
277 diffusion: 0.15,
278 decay: 0.0,
279 initial: 0.0,
280 }],
281 12,
282 );
283 fields.values[0][lattice.index(6, 6, 6)] = 100.0;
284 let before = fields.total(0);
285 for _ in 0..40 {
286 fields.diffuse(&lattice);
287 }
288 assert!((fields.total(0) - before).abs() < 1e-9);
289 assert!(
290 fields.values[0][lattice.index(6, 6, 7)] > 0.0,
291 "nothing moved in z"
292 );
293 }
294
295 #[test]
296 fn a_field_stays_stable_at_a_diffusion_constant_far_past_the_limit() {
297 let (mut fields, lattice) = flat(
298 vec![Species {
299 name: "a".into(),
300 diffusion: 10.0,
301 decay: 0.0,
302 initial: 0.0,
303 }],
304 32,
305 32,
306 );
307 fields.values[0][16 * 32 + 16] = 100.0;
308 for _ in 0..20 {
309 fields.diffuse(&lattice);
310 }
311 assert!(
312 fields.values[0]
313 .iter()
314 .all(|v| v.is_finite() && *v >= -1e-12),
315 "an explicit solver past its limit blows up rather than merely losing accuracy"
316 );
317 }
318
319 #[test]
320 fn secretion_and_uptake_follow_the_owning_cell() {
321 let mut lattice = Lattice::medium(8, 8, 1, 2);
322 lattice.labels[0] = 1;
323 let mut fields = Fields::new(
324 vec![Species {
325 name: "a".into(),
326 diffusion: 0.0,
327 decay: 0.0,
328 initial: 1.0,
329 }],
330 64,
331 2,
332 );
333 let exchange = vec![
334 Exchange::default(),
335 Exchange {
336 secretion: vec![2.0],
337 uptake: vec![0.5],
338 },
339 ];
340 fields.exchange(&lattice, &[0, 1], &exchange);
341 assert!((fields.values[0][0] - 1.5).abs() < 1e-12);
343 assert!((fields.values[0][1] - 1.0).abs() < 1e-12);
345 }
346}