Skip to main content

glazier_core/
cpu.rs

1//! Serial Metropolis on the CPU. This is the correctness reference.
2
3use crate::field::{Exchange, Fields};
4use crate::lattice::Lattice;
5use crate::model::Model;
6use crate::moments::{Moments, Site};
7use crate::motility::Activity;
8use crate::rng::Xoshiro;
9
10/// Second moments per label, unwrapped about each cell's first site.
11fn count_moments(lattice: &Lattice, labels: usize) -> Vec<Moments> {
12    let mut moments = vec![Moments::default(); labels];
13    let extent = lattice_extent(lattice);
14    for index in 0..lattice.labels.len() {
15        let label = lattice.labels[index] as usize;
16        let (x, y, z) = lattice.coords(index);
17        let site = moments[label].unwrap(x as f64, y as f64, z as f64, extent);
18        moments[label].add(site);
19    }
20    moments
21}
22
23/// The lattice sides, for the minimum image convention.
24fn lattice_extent(lattice: &Lattice) -> (f64, f64, f64) {
25    (
26        lattice.width as f64,
27        lattice.height as f64,
28        lattice.depth as f64,
29    )
30}
31
32/// Bonds from each label's sites to sites of any other label.
33fn count_surface(lattice: &Lattice, labels: usize) -> Vec<i64> {
34    let mut surface = vec![0i64; labels];
35    for index in 0..lattice.labels.len() {
36        let (x, y, z) = lattice.coords(index);
37        let l = lattice.labels[index];
38        for &(dx, dy, dz) in &lattice.offsets {
39            let n = lattice.index(x + dx, y + dy, z + dz);
40            if n != index && lattice.labels[n] != l {
41                surface[l as usize] += 1;
42            }
43        }
44    }
45    surface
46}
47
48/// A running simulation: the lattice, one volume and one type per cell, and
49/// the random stream.
50#[derive(Clone, Debug)]
51pub struct Simulation {
52    /// The model this run realises.
53    pub model: Model,
54    /// The site labels.
55    pub lattice: Lattice,
56    /// Volume per label. Index 0 is the medium, counted like any other so the
57    /// total over the vector is the lattice, and never constrained.
58    pub volume: Vec<u32>,
59    /// Surface per label, counted as bonds from a cell's sites to sites of any
60    /// other label, over the model's own neighbour order.
61    pub surface: Vec<i64>,
62    /// Type per label.
63    pub cell_type: Vec<u8>,
64    /// Second moments per label, for the length constraint.
65    pub moments: Vec<Moments>,
66    /// How recently each site was taken, for the types that keep a memory.
67    pub activity: Activity,
68    /// Diffusing species on the same lattice.
69    pub fields: Fields,
70    /// Secretion and uptake per cell type.
71    pub exchange: Vec<Exchange>,
72    /// Whether any type constrains its length, read once rather than scanned
73    /// on every copy attempt.
74    constrains_length: bool,
75    /// The neighbourhood the connectivity test reads, empty when no type asks
76    /// for one.
77    connectivity_ring: Vec<(i64, i64, i64)>,
78    rng: Xoshiro,
79    /// Monte Carlo steps completed.
80    pub mcs: u64,
81    /// Copy attempts accepted since the run started.
82    pub accepted: u64,
83    /// Copy attempts made since the run started.
84    pub attempted: u64,
85}
86
87impl Simulation {
88    /// Start a run from a tiled lattice of square cells, all of type 1.
89    ///
90    /// # Errors
91    /// Whatever [`Model::validate`] reports.
92    pub fn tiled(model: Model, side: usize) -> Result<Self, String> {
93        let (nx, ny, nz) = (
94            model.width / side,
95            model.height / side,
96            (model.depth / side).max(1),
97        );
98        Self::tiled_grid(model, side, nx, ny, nz)
99    }
100
101    /// Start a run from `nx` by `ny` square cells, leaving the rest medium.
102    ///
103    /// # Errors
104    /// Whatever [`Model::validate`] reports.
105    pub fn tiled_grid(
106        model: Model,
107        side: usize,
108        nx: usize,
109        ny: usize,
110        nz: usize,
111    ) -> Result<Self, String> {
112        model.validate()?;
113        if model.n_types() < 2 {
114            return Err("a tiled start needs a cell type beside the medium".into());
115        }
116        let mut lattice = Lattice::medium(
117            model.width,
118            model.height,
119            model.depth,
120            model.neighbour_order,
121        );
122        let n = lattice.tile_grid(side, nx, ny, nz) as usize;
123        let mut volume = vec![0u32; n + 1];
124        for &label in &lattice.labels {
125            volume[label as usize] += 1;
126        }
127        let surface = count_surface(&lattice, n + 1);
128        let moments = count_moments(&lattice, n + 1);
129        let sites = lattice.labels.len();
130        let species = model.species.clone();
131        let dimensions = model.dimensions();
132        let constrains_length = model.has_length_constraint();
133        let connectivity_ring = if model.has_connectivity() {
134            crate::connectivity::ring(model.depth)
135        } else {
136            Vec::new()
137        };
138        let exchange = model.exchange.clone();
139        let seed = model.seed;
140        Ok(Self {
141            model,
142            lattice,
143            volume,
144            surface,
145            cell_type: vec![1; n + 1],
146            moments,
147            activity: Activity::new(sites),
148            fields: Fields::new(species, sites, dimensions),
149            exchange,
150            constrains_length,
151            connectivity_ring,
152            rng: Xoshiro::seed(seed),
153            mcs: 0,
154            accepted: 0,
155            attempted: 0,
156        })
157    }
158
159    /// Set the type of each cell, in label order, from a list one per cell.
160    pub fn set_cell_types(&mut self, types: &[u8]) {
161        for (label, &kind) in types.iter().enumerate() {
162            self.cell_type[label + 1] = kind;
163        }
164    }
165
166    /// Number of cells, excluding the medium.
167    #[must_use]
168    pub fn n_cells(&self) -> usize {
169        self.volume.len() - 1
170    }
171
172    fn type_of(&self, label: u32) -> u8 {
173        self.cell_type[label as usize]
174    }
175
176    /// Energy change if site `target` took the label `new`.
177    ///
178    /// The contact term counts only unlike-label bonds, which is the
179    /// `(1 - delta)` factor of the Potts Hamiltonian. The volume term is the
180    /// change in `lambda (V - V_target)^2` for the two cells involved.
181    #[must_use]
182    pub fn delta_energy(&self, target: usize, new: u32) -> f64 {
183        let old = self.lattice.labels[target];
184        if old == new {
185            return 0.0;
186        }
187        let (tx, ty, tz) = self.lattice.coords(target);
188        let (told, tnew) = (self.type_of(old), self.type_of(new));
189
190        let mut delta = 0.0;
191        let mut like_old = 0i64;
192        let mut like_new = 0i64;
193        let mut bonds = 0i64;
194        for &(dx, dy, dz) in &self.lattice.offsets {
195            let n = self.lattice.index(tx + dx, ty + dy, tz + dz);
196            if n == target {
197                continue;
198            }
199            bonds += 1;
200            let ln = self.lattice.labels[n];
201            let tn = self.type_of(ln);
202            if ln != old {
203                delta -= self.model.contact_energy(told, tn);
204            } else {
205                like_old += 1;
206            }
207            if ln != new {
208                delta += self.model.contact_energy(tnew, tn);
209            } else {
210                like_new += 1;
211            }
212        }
213
214        delta += self.volume_term(old, -1);
215        delta += self.volume_term(new, 1);
216        if self.constrains_length {
217            let site = (tx as f64, ty as f64, tz as f64);
218            delta += self.length_term(old, site, -1.0);
219            delta += self.length_term(new, site, 1.0);
220        }
221        delta += self.surface_term(old, 2 * like_old - bonds);
222        delta += self.surface_term(new, bonds - 2 * like_new);
223        delta
224    }
225
226    /// Change in the length energy of `label` when a site is added or removed.
227    fn length_term(&self, label: u32, site: (f64, f64, f64), sign: f64) -> f64 {
228        if label == 0 {
229            return 0.0;
230        }
231        let spec = self.model.types[self.type_of(label) as usize];
232        if spec.lambda_length == 0.0 {
233            return 0.0;
234        }
235        let moments = self.moments[label as usize];
236        let unwrapped = moments.unwrap(site.0, site.1, site.2, lattice_extent(&self.lattice));
237        let before = moments.length();
238        let after = moments.with(unwrapped, sign).length();
239        spec.lambda_length
240            * ((after - spec.target_length).powi(2) - (before - spec.target_length).powi(2))
241    }
242
243    /// Work the cell gaining the site does against its own memory and its
244    /// drift.
245    ///
246    /// Both read the two sites a copy runs between, so both are properties of
247    /// the move rather than of the configuration and neither appears in
248    /// [`Simulation::energy`].
249    #[must_use]
250    pub fn move_work(&self, new: u32, old: u32, target: usize, source: usize) -> f64 {
251        let mut work = 0.0;
252
253        if new != 0 {
254            let spec = self.model.types[self.type_of(new) as usize];
255            if spec.lambda_activity != 0.0 && spec.max_activity > 0.0 {
256                let into = self.activity.neighbourhood_mean(&self.lattice, source, new);
257                let out_of = self.activity.neighbourhood_mean(&self.lattice, target, old);
258                work -= spec.lambda_activity / spec.max_activity * (into - out_of);
259            }
260            if spec.external.iter().any(|&v| v != 0.0) {
261                let (tx, ty, tz) = self.lattice.coords(target);
262                let (sx, sy, sz) = self.lattice.coords(source);
263                let (w, h, d) = lattice_extent(&self.lattice);
264                let step = |a: i64, b: i64, span: f64| {
265                    let raw = (a - b) as f64;
266                    if raw > span / 2.0 {
267                        raw - span
268                    } else if raw < -span / 2.0 {
269                        raw + span
270                    } else {
271                        raw
272                    }
273                };
274                work -= spec.external[0] * step(tx, sx, w)
275                    + spec.external[1] * step(ty, sy, h)
276                    + spec.external[2] * step(tz, sz, d);
277            }
278        }
279        work
280    }
281
282    /// Work done against a chemical gradient by the cell gaining the site.
283    ///
284    /// This is a property of the move rather than of the configuration: it
285    /// reads the field at the two sites the copy runs between, so it has no
286    /// counterpart in [`Simulation::energy`] and cannot be checked by
287    /// recomputing a total. A positive sensitivity makes a move up the
288    /// gradient cheaper, which is what makes a cell climb it.
289    #[must_use]
290    pub fn chemotaxis_work(&self, new: u32, target: usize, source: usize) -> f64 {
291        if new == 0 || self.model.chemotaxis.is_empty() {
292            return 0.0;
293        }
294        let row = &self.model.chemotaxis[self.type_of(new) as usize];
295        let mut work = 0.0;
296        for (index, &lambda) in row.iter().enumerate() {
297            if lambda == 0.0 {
298                continue;
299            }
300            let values = &self.fields.values[index];
301            work -= lambda * (values[target] - values[source]);
302        }
303        work
304    }
305
306    /// Change in the surface energy of `label` when its surface moves by
307    /// `change` bonds.
308    fn surface_term(&self, label: u32, change: i64) -> f64 {
309        if label == 0 || change == 0 {
310            return 0.0;
311        }
312        let spec = self.model.types[self.type_of(label) as usize];
313        if spec.lambda_surface == 0.0 {
314            return 0.0;
315        }
316        let s = self.surface[label as usize] as f64;
317        let after = s + change as f64;
318        spec.lambda_surface
319            * ((after - spec.target_surface).powi(2) - (s - spec.target_surface).powi(2))
320    }
321
322    fn volume_term(&self, label: u32, change: i64) -> f64 {
323        if label == 0 {
324            return 0.0;
325        }
326        let spec = self.model.types[self.type_of(label) as usize];
327        if spec.lambda_volume == 0.0 {
328            return 0.0;
329        }
330        let v = f64::from(self.volume[label as usize]);
331        let after = v + change as f64;
332        spec.lambda_volume
333            * ((after - spec.target_volume).powi(2) - (v - spec.target_volume).powi(2))
334    }
335
336    /// One copy attempt: a random target site takes the label of a random
337    /// neighbour, accepted by the Metropolis rule.
338    ///
339    /// A copy that would empty a cell is refused, so a cell never vanishes and
340    /// the label set is fixed for the run.
341    pub fn attempt(&mut self) -> bool {
342        let sites = self.lattice.labels.len() as u64;
343        let target = self.rng.below(sites) as usize;
344        let pick = self.rng.below(self.lattice.offsets.len() as u64) as usize;
345        let (tx, ty, tz) = self.lattice.coords(target);
346        let (dx, dy, dz) = self.lattice.offsets[pick];
347        let source = self.lattice.index(tx + dx, ty + dy, tz + dz);
348
349        let old = self.lattice.labels[target];
350        let new = self.lattice.labels[source];
351        self.attempted += 1;
352        if old == new {
353            return false;
354        }
355        if old != 0 && self.volume[old as usize] <= 1 {
356            return false;
357        }
358        if old != 0
359            && !self.connectivity_ring.is_empty()
360            && self.model.types[self.type_of(old) as usize].connected
361            && !crate::connectivity::locally_connected(
362                &self.lattice,
363                &self.connectivity_ring,
364                target,
365                old,
366            )
367        {
368            return false;
369        }
370
371        let delta = self.delta_energy(target, new)
372            + self.chemotaxis_work(new, target, source)
373            + self.move_work(new, old, target, source);
374        let accept = delta <= 0.0 || self.rng.next_f64() < (-delta / self.model.temperature).exp();
375        if accept {
376            let (tx, ty, tz) = self.lattice.coords(target);
377            let mut like_old = 0i64;
378            let mut like_new = 0i64;
379            let mut bonds = 0i64;
380            for &(dx, dy, dz) in &self.lattice.offsets {
381                let n = self.lattice.index(tx + dx, ty + dy, tz + dz);
382                if n == target {
383                    continue;
384                }
385                bonds += 1;
386                let ln = self.lattice.labels[n];
387                if ln == old {
388                    like_old += 1;
389                }
390                if ln == new {
391                    like_new += 1;
392                }
393            }
394            self.surface[old as usize] += 2 * like_old - bonds;
395            self.surface[new as usize] += bonds - 2 * like_new;
396
397            if self.constrains_length {
398                let site = (tx as f64, ty as f64, tz as f64);
399                let extent = lattice_extent(&self.lattice);
400                let leaving = self.moments[old as usize].unwrap(site.0, site.1, site.2, extent);
401                self.moments[old as usize].remove(leaving);
402                let joining = self.moments[new as usize].unwrap(site.0, site.1, site.2, extent);
403                self.moments[new as usize].add(joining);
404            }
405
406            if new != 0 {
407                let spec = self.model.types[self.type_of(new) as usize];
408                if spec.max_activity > 0.0 {
409                    self.activity.refresh(target, spec.max_activity);
410                }
411            }
412
413            self.lattice.labels[target] = new;
414            self.volume[old as usize] -= 1;
415            self.volume[new as usize] += 1;
416            self.accepted += 1;
417        }
418        accept
419    }
420
421    /// One Monte Carlo step: as many copy attempts as there are sites, then
422    /// the fields diffuse, decay and exchange with the cells that own the
423    /// sites they sit on.
424    pub fn step(&mut self) {
425        for _ in 0..self.lattice.labels.len() {
426            self.attempt();
427        }
428        if !self.fields.is_empty() {
429            self.fields.diffuse(&self.lattice);
430            self.fields
431                .exchange(&self.lattice, &self.cell_type, &self.exchange);
432        }
433        if self.model.has_motility() {
434            self.activity.decay();
435        }
436        if self.model.has_population_events() {
437            self.divide_and_die();
438        }
439        self.mcs += 1;
440    }
441
442    /// Total energy of the current configuration.
443    ///
444    /// Each unlike-label bond is counted once, so this is comparable between
445    /// neighbour orders and is what the incremental deltas must reproduce.
446    #[must_use]
447    pub fn energy(&self) -> f64 {
448        let mut contact = 0.0;
449        for index in 0..self.lattice.labels.len() {
450            let (x, y, z) = self.lattice.coords(index);
451            let l = self.lattice.labels[index];
452            for &(dx, dy, dz) in &self.lattice.offsets {
453                let n = self.lattice.index(x + dx, y + dy, z + dz);
454                let ln = self.lattice.labels[n];
455                if ln != l {
456                    contact += self.model.contact_energy(self.type_of(l), self.type_of(ln));
457                }
458            }
459        }
460        contact /= 2.0;
461
462        let mut volume = 0.0;
463        let mut surface = 0.0;
464        let mut length = 0.0;
465        let counted = self.recounted_surfaces();
466        for (label, &bonds) in counted.iter().enumerate().skip(1) {
467            let spec = self.model.types[self.cell_type[label] as usize];
468            volume +=
469                spec.lambda_volume * (f64::from(self.volume[label]) - spec.target_volume).powi(2);
470            if spec.lambda_length != 0.0 {
471                length += spec.lambda_length
472                    * (self.moments[label].length() - spec.target_length).powi(2);
473            }
474            surface += spec.lambda_surface * (bonds as f64 - spec.target_surface).powi(2);
475        }
476        contact + volume + surface + length
477    }
478
479    /// Recompute the moments from the lattice, for checking the bookkeeping.
480    #[must_use]
481    pub fn recounted_moments(&self) -> Vec<Moments> {
482        count_moments(&self.lattice, self.moments.len())
483    }
484
485    /// Recount the surfaces from the lattice, for checking the bookkeeping.
486    #[must_use]
487    pub fn recounted_surfaces(&self) -> Vec<i64> {
488        count_surface(&self.lattice, self.surface.len())
489    }
490
491    /// Recount the volumes from the lattice, for checking the bookkeeping.
492    #[must_use]
493    pub fn recounted_volumes(&self) -> Vec<u32> {
494        let mut v = vec![0u32; self.volume.len()];
495        for &label in &self.lattice.labels {
496            v[label as usize] += 1;
497        }
498        v
499    }
500}
501
502impl Simulation {
503    /// Cells that have reached their division volume split, and cells of a
504    /// type with a death rate die.
505    ///
506    /// A dividing cell is cut by the line through its centroid perpendicular
507    /// to its major axis, so the halves are the compact ones. A dying cell's
508    /// sites become medium. Both change the boundary of every neighbour, so
509    /// the surfaces are recounted whenever either happens rather than patched.
510    pub fn divide_and_die(&mut self) -> (usize, usize) {
511        let mut divided = 0usize;
512        let mut died = 0usize;
513
514        let live: Vec<u32> = (1..self.volume.len() as u32)
515            .filter(|&label| self.volume[label as usize] > 0)
516            .collect();
517
518        for label in live {
519            let spec = self.model.types[self.type_of(label) as usize];
520            if spec.division_volume > 0.0
521                && f64::from(self.volume[label as usize]) >= spec.division_volume
522                && self.divide(label)
523            {
524                divided += 1;
525            }
526        }
527
528        let live: Vec<u32> = (1..self.volume.len() as u32)
529            .filter(|&label| self.volume[label as usize] > 0)
530            .collect();
531        for label in live {
532            let spec = self.model.types[self.type_of(label) as usize];
533            if spec.death_rate > 0.0 && self.rng.next_f64() < spec.death_rate {
534                self.kill(label);
535                died += 1;
536            }
537        }
538
539        if divided > 0 || died > 0 {
540            self.surface = self.recounted_surfaces();
541            self.moments = self.recounted_moments();
542        }
543        (divided, died)
544    }
545
546    /// Sites of a label, unwrapped about the first one so a cell straddling
547    /// the periodic edge still has a centroid and an axis.
548    fn unwrapped_sites(&self, label: u32) -> (Vec<(usize, Site)>, Moments) {
549        let extent = lattice_extent(&self.lattice);
550        let mut moments = Moments::default();
551        let mut out: Vec<(usize, Site)> = Vec::new();
552        for (site, &l) in self.lattice.labels.iter().enumerate() {
553            if l != label {
554                continue;
555            }
556            let (x, y, z) = self.lattice.coords(site);
557            let unwrapped = moments.unwrap(x as f64, y as f64, z as f64, extent);
558            moments.add(unwrapped);
559            out.push((site, unwrapped));
560        }
561        (out, moments)
562    }
563
564    /// Split one cell in two. Returns whether it happened.
565    ///
566    /// The cut is the plane through the centroid perpendicular to the major
567    /// axis, so the halves are the compact ones whatever the dimension.
568    fn divide(&mut self, label: u32) -> bool {
569        let (sites, moments) = self.unwrapped_sites(label);
570        if sites.len() < 4 {
571            return false;
572        }
573
574        let centre = moments.centroid();
575        let (axis, _) = moments.principal();
576
577        let daughter = self.volume.len() as u32;
578        let mut moved = 0u32;
579        for &(site, (x, y, z)) in &sites {
580            let along = (x - centre.0) * axis.0 + (y - centre.1) * axis.1 + (z - centre.2) * axis.2;
581            if along > 0.0 {
582                self.lattice.labels[site] = daughter;
583                moved += 1;
584            }
585        }
586        if moved == 0 || moved as usize == sites.len() {
587            // A cut that takes everything or nothing is no division.
588            for &(site, _) in &sites {
589                self.lattice.labels[site] = label;
590            }
591            return false;
592        }
593
594        self.volume[label as usize] -= moved;
595        self.volume.push(moved);
596        self.surface.push(0);
597        self.moments.push(Moments::default());
598        self.cell_type.push(self.type_of(label));
599        true
600    }
601
602    /// Turn a cell's sites back into medium.
603    fn kill(&mut self, label: u32) {
604        for site in 0..self.lattice.labels.len() {
605            if self.lattice.labels[site] == label {
606                self.lattice.labels[site] = 0;
607            }
608        }
609        let gone = self.volume[label as usize];
610        self.volume[label as usize] = 0;
611        self.volume[0] += gone;
612        self.surface[label as usize] = 0;
613    }
614
615    /// Labels with sites on the lattice.
616    #[must_use]
617    pub fn live_cells(&self) -> usize {
618        self.volume[1..].iter().filter(|&&v| v > 0).count()
619    }
620}