Skip to main content

glazier_core/
field.rs

1//! Diffusing chemical fields on the same lattice as the cells.
2//!
3//! One value per site per species. A step diffuses and decays the field by
4//! explicit forward Euler, then lets every cell secrete into the sites it owns
5//! and take up from them, which is the order CompuCell3D's solver uses.
6//!
7//! Explicit diffusion is stable while `D dt / dx^2` stays at or below
8//! `1 / (2 n)` for `n` dimensions, so a stated diffusion constant sets the
9//! number of sub-steps rather than the other way round: a model states physics
10//! and the engine finds a schedule that survives it.
11
12use crate::lattice::Lattice;
13
14/// One diffusing species.
15#[derive(Clone, Debug, PartialEq)]
16pub struct Species {
17    /// The name a description and an engine agree on.
18    pub name: String,
19    /// Diffusion constant in sites squared per Monte Carlo step.
20    pub diffusion: f64,
21    /// Fractional decay per Monte Carlo step.
22    pub decay: f64,
23    /// Uniform value at the start.
24    pub initial: f64,
25}
26
27/// Secretion and uptake for one cell type, in the order of `Fields::species`.
28#[derive(Clone, Debug, Default, PartialEq)]
29pub struct Exchange {
30    /// Amount added to each site the cell owns, per step.
31    pub secretion: Vec<f64>,
32    /// Fraction removed from each site the cell owns, per step.
33    pub uptake: Vec<f64>,
34}
35
36/// Every species, and the concentrations on the lattice.
37#[derive(Clone, Debug)]
38pub struct Fields {
39    /// The species, in the order a description lists them.
40    pub species: Vec<Species>,
41    /// One concentration grid per species, indexed as the lattice is.
42    pub values: Vec<Vec<f64>>,
43    /// Diffusion sub-steps per Monte Carlo step, one per species.
44    pub substeps: Vec<usize>,
45    scratch: Vec<f64>,
46}
47
48/// The largest `D dt / dx^2` an explicit Laplacian survives, by dimension.
49#[must_use]
50pub fn stability_limit(dimensions: usize) -> f64 {
51    1.0 / (2.0 * dimensions.max(1) as f64)
52}
53
54impl Fields {
55    /// Lay out the fields for a lattice.
56    #[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    /// Number of species.
75    #[must_use]
76    pub fn len(&self) -> usize {
77        self.species.len()
78    }
79
80    /// Whether the model states no field at all.
81    #[must_use]
82    pub fn is_empty(&self) -> bool {
83        self.species.is_empty()
84    }
85
86    /// Index of a species by name.
87    #[must_use]
88    pub fn index_of(&self, name: &str) -> Option<usize> {
89        self.species.iter().position(|s| s.name == name)
90    }
91
92    /// Total amount of a species over the lattice.
93    #[must_use]
94    pub fn total(&self, index: usize) -> f64 {
95        self.values[index].iter().sum()
96    }
97
98    /// Diffuse and decay every species by one Monte Carlo step.
99    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        // The face neighbours alone, which is the Laplacian this scheme is
112        // stable for; a plane simply has no pair along the third axis.
113        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    /// Secretion and uptake by whichever cell owns each site.
133    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/// Sub-steps needed to diffuse at `diffusion` sites squared per step and stay
153/// inside the stability limit for the stated dimension count.
154#[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        // The same constant needs half again as many sub-steps in a volume.
268        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        // The owned site takes 1 + 2 = 3, then loses half of it.
342        assert!((fields.values[0][0] - 1.5).abs() < 1e-12);
343        // Every other site is medium and untouched.
344        assert!((fields.values[0][1] - 1.0).abs() < 1e-12);
345    }
346}