Skip to main content

glazier_core/
blueprint.rs

1//! The model description both engines read.
2//!
3//! One file states the lattice, the cell types, the contact matrix, the
4//! initial condition, the schedule and the units. Anything an engine needs and
5//! this does not state is a gap in the format rather than a detail of the
6//! engine.
7
8use crate::field::{Exchange, Species};
9use crate::model::{CellType, Model};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13/// What one lattice site and one Monte Carlo step stand for.
14///
15/// A Potts lattice has no physical scale of its own, so a description that
16/// omits this cannot be compared with a model in microns and minutes.
17#[derive(Clone, Debug, Deserialize, Serialize)]
18pub struct Units {
19    /// Physical length of one lattice site.
20    pub micron_per_site: f64,
21    /// Physical duration of one Monte Carlo step.
22    pub minute_per_step: f64,
23}
24
25/// The depth a description that says nothing about it means.
26fn one() -> usize {
27    1
28}
29
30/// Adhesion molecules and how strongly each pair of them binds.
31///
32/// A cell type states how much of each molecule it presents, and the binding
33/// matrix says what a pair of them is worth. The contact energy between two
34/// types is then the stated one less what their molecules bind, which is a
35/// function of the two types alone and so collapses into the contact matrix
36/// before an engine ever sees it.
37///
38/// Per-cell adhesion, where two cells of one type present different amounts
39/// and the amounts change as the cell runs, is a different thing and is not
40/// this.
41#[derive(Clone, Debug, Deserialize, Serialize)]
42pub struct Adhesion {
43    /// The molecules, in the order the binding matrix indexes them.
44    pub molecules: Vec<String>,
45    /// Binding energies, row-major over the molecules and symmetric. A
46    /// positive entry binds, which lowers the contact energy.
47    pub binding: Vec<f64>,
48}
49
50/// A diffusing species as the description states it.
51#[derive(Clone, Debug, Deserialize, Serialize)]
52pub struct SpeciesSpec {
53    /// The name every engine refers to it by.
54    pub name: String,
55    /// Diffusion constant in sites squared per Monte Carlo step.
56    pub diffusion: f64,
57    /// Fractional decay per step.
58    #[serde(default)]
59    pub decay: f64,
60    /// Uniform concentration at the start.
61    #[serde(default)]
62    pub initial: f64,
63}
64
65/// A cell type as the description states it.
66#[derive(Clone, Debug, Deserialize, Serialize)]
67pub struct TypeSpec {
68    /// The name an engine shows.
69    pub name: String,
70    /// Target volume in lattice sites.
71    pub target_volume: f64,
72    /// Volume constraint strength.
73    pub lambda_volume: f64,
74    /// Amount of each named species added to every site the cell owns, per
75    /// step. A species the map leaves out is not secreted.
76    #[serde(default)]
77    pub secretion: BTreeMap<String, f64>,
78    /// Fraction of each named species removed from every site the cell owns,
79    /// per step.
80    #[serde(default)]
81    pub uptake: BTreeMap<String, f64>,
82    /// Sensitivity to each named species' gradient. Positive climbs it.
83    #[serde(default)]
84    pub chemotaxis: BTreeMap<String, f64>,
85    /// How much of each adhesion molecule this type presents.
86    #[serde(default)]
87    pub presents: BTreeMap<String, f64>,
88    /// Target surface in boundary bonds. Zero with `lambda_surface` zero
89    /// leaves the constraint out, which is what a description that says
90    /// nothing about surface means.
91    #[serde(default)]
92    pub target_surface: f64,
93    /// Surface constraint strength.
94    #[serde(default)]
95    pub lambda_surface: f64,
96    /// Volume at which a cell divides, in sites. Absent never divides.
97    #[serde(default)]
98    pub division_volume: f64,
99    /// Probability per step that a cell of this type dies. Absent never dies.
100    #[serde(default)]
101    pub death_rate: f64,
102    /// Target major axis in sites. Absent leaves the length unconstrained.
103    #[serde(default)]
104    pub target_length: f64,
105    /// Length constraint strength.
106    #[serde(default)]
107    pub lambda_length: f64,
108    /// Refuse a copy that would locally pinch a cell of this type in two.
109    #[serde(default)]
110    pub connected: bool,
111    /// Steps of memory a site keeps after this cell takes it.
112    #[serde(default)]
113    pub max_activity: f64,
114    /// Strength of the persistence that memory buys.
115    #[serde(default)]
116    pub lambda_activity: f64,
117    /// A constant drift, stated per axis.
118    #[serde(default)]
119    pub external: [f64; 3],
120}
121
122/// The initial condition. Squares laid down from the origin, all of the first
123/// type unless `fractions` says otherwise.
124#[derive(Clone, Debug, Deserialize, Serialize)]
125pub struct Initial {
126    /// Side of each square in sites.
127    pub side: usize,
128    /// Squares across.
129    pub nx: usize,
130    /// Squares down.
131    pub ny: usize,
132    /// Cubes through the depth. Absent is one layer.
133    #[serde(default = "one")]
134    pub nz: usize,
135    /// Fraction of the cells to start as each named type. What the map leaves
136    /// unassigned starts as the first type the description lists, so an
137    /// infection seeded at two percent states one number.
138    #[serde(default)]
139    pub fractions: BTreeMap<String, f64>,
140}
141
142/// The whole description.
143#[derive(Clone, Debug, Deserialize, Serialize)]
144pub struct Blueprint {
145    /// A name for the model.
146    pub name: String,
147    /// Lattice width in sites.
148    pub width: usize,
149    /// Lattice height in sites.
150    pub height: usize,
151    /// Lattice depth in sites. Absent is a plane.
152    #[serde(default = "one")]
153    pub depth: usize,
154    /// Cell types beside the medium, in order.
155    pub types: Vec<TypeSpec>,
156    /// Diffusing species, in order.
157    #[serde(default)]
158    pub fields: Vec<SpeciesSpec>,
159    /// Adhesion molecules and their binding matrix, absent when the contact
160    /// matrix says everything.
161    #[serde(default)]
162    pub adhesion: Option<Adhesion>,
163    /// Contact energies over `["Medium", types...]`, row-major and symmetric.
164    pub contact: Vec<f64>,
165    /// Metropolis fluctuation amplitude.
166    pub temperature: f64,
167    /// 1 for four neighbours, 2 for eight.
168    pub neighbour_order: u8,
169    /// Random seed.
170    pub seed: u64,
171    /// Monte Carlo steps to run.
172    pub steps: u64,
173    /// Steps between label-field dumps.
174    pub dump_every: u64,
175    /// The initial condition.
176    pub initial: Initial,
177    /// The physical scale.
178    pub units: Units,
179}
180
181impl Blueprint {
182    /// The contact matrix with the adhesion molecules folded in.
183    ///
184    /// Binding lowers the energy of a bond, so what two types' molecules bind
185    /// comes off the contact energy the description states between them.
186    #[must_use]
187    pub fn effective_contact(&self) -> Vec<f64> {
188        let mut contact = self.contact.clone();
189        let Some(adhesion) = &self.adhesion else {
190            return contact;
191        };
192
193        let n_types = self.types.len() + 1;
194        let presented = |index: usize| -> Vec<f64> {
195            if index == 0 {
196                return vec![0.0; adhesion.molecules.len()];
197            }
198            adhesion
199                .molecules
200                .iter()
201                .map(|name| {
202                    self.types[index - 1]
203                        .presents
204                        .get(name)
205                        .copied()
206                        .unwrap_or(0.0)
207                })
208                .collect()
209        };
210
211        let molecules: Vec<Vec<f64>> = (0..n_types).map(presented).collect();
212        let n = adhesion.molecules.len();
213        for a in 0..n_types {
214            for b in 0..n_types {
215                let mut bound = 0.0;
216                for i in 0..n {
217                    for j in 0..n {
218                        bound += adhesion.binding[i * n + j] * molecules[a][i] * molecules[b][j];
219                    }
220                }
221                contact[a * n_types + b] -= bound;
222            }
223        }
224        contact
225    }
226
227    /// Type index of each cell at the start, by the fractions stated.
228    ///
229    /// Assignment walks the cells in order and hands out each type its share,
230    /// so the same description and the same cell count always start the same
231    /// way whatever an engine's own random stream does.
232    #[must_use]
233    pub fn initial_types(&self, cells: usize) -> Vec<u8> {
234        let mut types = vec![1u8; cells];
235        let mut next = 0usize;
236        for (index, spec) in self.types.iter().enumerate() {
237            let Some(&fraction) = self.initial.fractions.get(&spec.name) else {
238                continue;
239            };
240            let share = ((fraction * cells as f64).round() as usize).min(cells - next);
241            for slot in types.iter_mut().skip(next).take(share) {
242                *slot = index as u8 + 1;
243            }
244            next += share;
245        }
246        types
247    }
248
249    /// The engine-facing model this description states.
250    #[must_use]
251    pub fn model(&self) -> Model {
252        let mut types = vec![CellType::default()];
253        types.extend(self.types.iter().map(|t| CellType {
254            target_volume: t.target_volume,
255            lambda_volume: t.lambda_volume,
256            target_surface: t.target_surface,
257            lambda_surface: t.lambda_surface,
258            division_volume: t.division_volume,
259            death_rate: t.death_rate,
260            target_length: t.target_length,
261            lambda_length: t.lambda_length,
262            connected: t.connected,
263            max_activity: t.max_activity,
264            lambda_activity: t.lambda_activity,
265            external: t.external,
266        }));
267        let species: Vec<Species> = self
268            .fields
269            .iter()
270            .map(|f| Species {
271                name: f.name.clone(),
272                diffusion: f.diffusion,
273                decay: f.decay,
274                initial: f.initial,
275            })
276            .collect();
277
278        let rates = |map: &BTreeMap<String, f64>| -> Vec<f64> {
279            species
280                .iter()
281                .map(|s| map.get(&s.name).copied().unwrap_or(0.0))
282                .collect()
283        };
284        let mut exchange = vec![Exchange {
285            secretion: vec![0.0; species.len()],
286            uptake: vec![0.0; species.len()],
287        }];
288        exchange.extend(self.types.iter().map(|t| Exchange {
289            secretion: rates(&t.secretion),
290            uptake: rates(&t.uptake),
291        }));
292
293        let mut chemotaxis = vec![vec![0.0; species.len()]];
294        chemotaxis.extend(self.types.iter().map(|t| rates(&t.chemotaxis)));
295
296        Model {
297            species,
298            exchange,
299            chemotaxis,
300            contact: self.effective_contact(),
301            width: self.width,
302            height: self.height,
303            depth: self.depth,
304            types,
305            temperature: self.temperature,
306            neighbour_order: self.neighbour_order,
307            seed: self.seed,
308        }
309    }
310
311    /// Read a description from JSON.
312    ///
313    /// # Errors
314    /// A message naming the parse failure or the inconsistency found. A
315    /// secretion or uptake entry naming a species the description does not
316    /// state is an error rather than a silent zero, since a typo there would
317    /// otherwise read as a cell that secretes nothing.
318    pub fn from_json(text: &str) -> Result<Self, String> {
319        let bp: Self = serde_json::from_str(text).map_err(|e| e.to_string())?;
320        let names: Vec<&str> = bp.types.iter().map(|t| t.name.as_str()).collect();
321        for name in bp.initial.fractions.keys() {
322            if !names.contains(&name.as_str()) {
323                return Err(format!(
324                    "the initial condition names the type {name}, which the description does \
325                     not state"
326                ));
327            }
328        }
329        if let Some(adhesion) = &bp.adhesion {
330            let n = adhesion.molecules.len();
331            if adhesion.binding.len() != n * n {
332                return Err(format!(
333                    "the binding matrix is {} entries for {n} molecules, want {}",
334                    adhesion.binding.len(),
335                    n * n
336                ));
337            }
338            for spec in &bp.types {
339                for name in spec.presents.keys() {
340                    if !adhesion.molecules.contains(name) {
341                        return Err(format!(
342                            "type {} presents {name}, which is not a molecule this description \
343                             declares",
344                            spec.name
345                        ));
346                    }
347                }
348            }
349        } else {
350            for spec in &bp.types {
351                if !spec.presents.is_empty() {
352                    return Err(format!(
353                        "type {} presents adhesion molecules, and the description declares none",
354                        spec.name
355                    ));
356                }
357            }
358        }
359
360        let known: Vec<&str> = bp.fields.iter().map(|f| f.name.as_str()).collect();
361        for spec in &bp.types {
362            for (what, map) in [
363                ("secretion", &spec.secretion),
364                ("uptake", &spec.uptake),
365                ("chemotaxis", &spec.chemotaxis),
366            ] {
367                for name in map.keys() {
368                    if !known.contains(&name.as_str()) {
369                        return Err(format!(
370                            "type {} states {what} of {name}, which is not a field this \
371                             description declares",
372                            spec.name
373                        ));
374                    }
375                }
376            }
377        }
378        bp.model().validate()?;
379        Ok(bp)
380    }
381}