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}