Skip to main content

glazier_core/
model.rs

1//! Model description: cell types, the contact matrix and the constraints.
2
3/// A cell type's own parameters. Type 0 is the medium and takes no volume
4/// constraint.
5///
6/// Every term is off at zero, so `CellType::default()` is the medium and a
7/// type states only what it uses. New terms therefore leave existing
8/// descriptions and existing callers alone.
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10pub struct CellType {
11    /// Target volume in lattice sites.
12    pub target_volume: f64,
13    /// Volume constraint strength.
14    pub lambda_volume: f64,
15    /// Target surface in boundary bonds.
16    pub target_surface: f64,
17    /// Surface constraint strength.
18    pub lambda_surface: f64,
19    /// Volume at which a cell divides. Zero never divides.
20    pub division_volume: f64,
21    /// Probability per step that a cell of this type dies. Zero never dies.
22    pub death_rate: f64,
23    /// Target major axis in sites. Zero leaves the length unconstrained.
24    pub target_length: f64,
25    /// Length constraint strength.
26    pub lambda_length: f64,
27    /// Whether a copy that would locally pinch this cell is refused.
28    pub connected: bool,
29    /// Steps of memory a site keeps after this cell takes it. Zero leaves the
30    /// cell with no persistence.
31    pub max_activity: f64,
32    /// Strength of the persistence.
33    pub lambda_activity: f64,
34    /// A constant drift, as a force per axis. The work is the displacement
35    /// along it, so a cell with one travels that way whatever its neighbours
36    /// do.
37    pub external: [f64; 3],
38}
39
40/// The whole model: what a Blueprint would state, in one struct.
41#[derive(Clone, Debug)]
42pub struct Model {
43    /// Diffusing species, in the order a description lists them.
44    pub species: Vec<crate::field::Species>,
45    /// Secretion and uptake per cell type, index 0 the medium.
46    pub exchange: Vec<crate::field::Exchange>,
47    /// Chemotactic sensitivity per cell type and species, index 0 the medium.
48    /// Positive climbs a gradient.
49    pub chemotaxis: Vec<Vec<f64>>,
50    /// Lattice width in sites.
51    pub width: usize,
52    /// Lattice height in sites.
53    pub height: usize,
54    /// Lattice depth in sites. One is a plane, and every neighbourhood then
55    /// drops its out-of-plane offsets.
56    pub depth: usize,
57    /// Contact energies, row-major over `(type_a, type_b)`, symmetric.
58    pub contact: Vec<f64>,
59    /// One entry per type, index 0 the medium.
60    pub types: Vec<CellType>,
61    /// Metropolis fluctuation amplitude, the Potts temperature.
62    pub temperature: f64,
63    /// 1 for the four von Neumann neighbours, 2 for the eight Moore ones.
64    pub neighbour_order: u8,
65    /// Random seed.
66    pub seed: u64,
67}
68
69impl Model {
70    /// Number of cell types, counting the medium.
71    #[must_use]
72    pub fn n_types(&self) -> usize {
73        self.types.len()
74    }
75
76    /// How many dimensions the lattice spans.
77    #[must_use]
78    pub fn dimensions(&self) -> usize {
79        if self.depth > 1 { 3 } else { 2 }
80    }
81
82    /// Whether any type keeps a memory of where it has been.
83    #[must_use]
84    pub fn has_motility(&self) -> bool {
85        self.types
86            .iter()
87            .any(|t| t.lambda_activity != 0.0 && t.max_activity > 0.0)
88    }
89
90    /// Whether any type drifts.
91    #[must_use]
92    pub fn has_external_potential(&self) -> bool {
93        self.types
94            .iter()
95            .any(|t| t.external.iter().any(|&v| v != 0.0))
96    }
97
98    /// Whether any type refuses a copy that would pinch it.
99    #[must_use]
100    pub fn has_connectivity(&self) -> bool {
101        self.types.iter().any(|t| t.connected)
102    }
103
104    /// Whether any type constrains its length, so a run without one skips the
105    /// moment bookkeeping.
106    #[must_use]
107    pub fn has_length_constraint(&self) -> bool {
108        self.types.iter().any(|t| t.lambda_length != 0.0)
109    }
110
111    /// Whether any type follows a gradient.
112    #[must_use]
113    pub fn has_chemotaxis(&self) -> bool {
114        self.chemotaxis
115            .iter()
116            .any(|row| row.iter().any(|&l| l != 0.0))
117    }
118
119    /// Whether any type divides or dies, so a run without either skips the
120    /// per-step scan over labels.
121    #[must_use]
122    pub fn has_population_events(&self) -> bool {
123        self.types
124            .iter()
125            .any(|t| t.division_volume > 0.0 || t.death_rate > 0.0)
126    }
127
128    /// Contact energy between two types.
129    #[must_use]
130    pub fn contact_energy(&self, a: u8, b: u8) -> f64 {
131        self.contact[a as usize * self.n_types() + b as usize]
132    }
133
134    /// Check the shapes agree before a run starts.
135    ///
136    /// # Errors
137    /// A message naming the first inconsistency found.
138    pub fn validate(&self) -> Result<(), String> {
139        let n = self.n_types();
140        if n == 0 {
141            return Err("a model needs at least the medium type".into());
142        }
143        if self.contact.len() != n * n {
144            return Err(format!(
145                "contact matrix is {} entries for {n} types, want {}",
146                self.contact.len(),
147                n * n
148            ));
149        }
150        for a in 0..n {
151            for b in 0..n {
152                let (ab, ba) = (self.contact[a * n + b], self.contact[b * n + a]);
153                if (ab - ba).abs() > 1e-12 {
154                    return Err(format!("contact matrix is asymmetric at ({a}, {b})"));
155                }
156            }
157        }
158        if self.width == 0 || self.height == 0 || self.depth == 0 {
159            return Err("the lattice has a zero dimension".into());
160        }
161        if !matches!(self.neighbour_order, 1..=3) {
162            return Err(format!(
163                "neighbour order {} is outside the range one to three",
164                self.neighbour_order
165            ));
166        }
167        if self.temperature <= 0.0 {
168            return Err("the temperature must be positive".into());
169        }
170        if !self.exchange.is_empty() && self.exchange.len() != n {
171            return Err(format!(
172                "exchange has {} entries for {n} types",
173                self.exchange.len()
174            ));
175        }
176        if !self.chemotaxis.is_empty() && self.chemotaxis.len() != n {
177            return Err(format!(
178                "chemotaxis has {} entries for {n} types",
179                self.chemotaxis.len()
180            ));
181        }
182        for (index, row) in self.chemotaxis.iter().enumerate() {
183            if !row.is_empty() && row.len() != self.species.len() {
184                return Err(format!(
185                    "type {index} states {} chemotactic sensitivities for {} species",
186                    row.len(),
187                    self.species.len()
188                ));
189            }
190        }
191        for (index, spec) in self.exchange.iter().enumerate() {
192            for (what, list) in [("secretion", &spec.secretion), ("uptake", &spec.uptake)] {
193                if !list.is_empty() && list.len() != self.species.len() {
194                    return Err(format!(
195                        "type {index} states {} {what} rates for {} species",
196                        list.len(),
197                        self.species.len()
198                    ));
199                }
200            }
201        }
202        Ok(())
203    }
204}
205
206impl Default for Model {
207    /// A one-site medium-only lattice at a temperature that runs. It exists so
208    /// a caller states the terms it cares about and leaves the rest, which is
209    /// what keeps a new energy term from touching every call site.
210    fn default() -> Self {
211        Self {
212            species: Vec::new(),
213            exchange: Vec::new(),
214            chemotaxis: Vec::new(),
215            width: 1,
216            height: 1,
217            depth: 1,
218            contact: vec![0.0],
219            types: vec![CellType::default()],
220            temperature: 10.0,
221            neighbour_order: 2,
222            seed: 0,
223        }
224    }
225}