use crate::field::{Exchange, Species};
use crate::model::{CellType, Model};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Units {
pub micron_per_site: f64,
pub minute_per_step: f64,
}
fn one() -> usize {
1
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Adhesion {
pub molecules: Vec<String>,
pub binding: Vec<f64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SpeciesSpec {
pub name: String,
pub diffusion: f64,
#[serde(default)]
pub decay: f64,
#[serde(default)]
pub initial: f64,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TypeSpec {
pub name: String,
pub target_volume: f64,
pub lambda_volume: f64,
#[serde(default)]
pub secretion: BTreeMap<String, f64>,
#[serde(default)]
pub uptake: BTreeMap<String, f64>,
#[serde(default)]
pub chemotaxis: BTreeMap<String, f64>,
#[serde(default)]
pub presents: BTreeMap<String, f64>,
#[serde(default)]
pub target_surface: f64,
#[serde(default)]
pub lambda_surface: f64,
#[serde(default)]
pub division_volume: f64,
#[serde(default)]
pub death_rate: f64,
#[serde(default)]
pub target_length: f64,
#[serde(default)]
pub lambda_length: f64,
#[serde(default)]
pub connected: bool,
#[serde(default)]
pub max_activity: f64,
#[serde(default)]
pub lambda_activity: f64,
#[serde(default)]
pub external: [f64; 3],
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Initial {
pub side: usize,
pub nx: usize,
pub ny: usize,
#[serde(default = "one")]
pub nz: usize,
#[serde(default)]
pub fractions: BTreeMap<String, f64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Blueprint {
pub name: String,
pub width: usize,
pub height: usize,
#[serde(default = "one")]
pub depth: usize,
pub types: Vec<TypeSpec>,
#[serde(default)]
pub fields: Vec<SpeciesSpec>,
#[serde(default)]
pub adhesion: Option<Adhesion>,
pub contact: Vec<f64>,
pub temperature: f64,
pub neighbour_order: u8,
pub seed: u64,
pub steps: u64,
pub dump_every: u64,
pub initial: Initial,
pub units: Units,
}
impl Blueprint {
#[must_use]
pub fn effective_contact(&self) -> Vec<f64> {
let mut contact = self.contact.clone();
let Some(adhesion) = &self.adhesion else {
return contact;
};
let n_types = self.types.len() + 1;
let presented = |index: usize| -> Vec<f64> {
if index == 0 {
return vec![0.0; adhesion.molecules.len()];
}
adhesion
.molecules
.iter()
.map(|name| {
self.types[index - 1]
.presents
.get(name)
.copied()
.unwrap_or(0.0)
})
.collect()
};
let molecules: Vec<Vec<f64>> = (0..n_types).map(presented).collect();
let n = adhesion.molecules.len();
for a in 0..n_types {
for b in 0..n_types {
let mut bound = 0.0;
for i in 0..n {
for j in 0..n {
bound += adhesion.binding[i * n + j] * molecules[a][i] * molecules[b][j];
}
}
contact[a * n_types + b] -= bound;
}
}
contact
}
#[must_use]
pub fn initial_types(&self, cells: usize) -> Vec<u8> {
let mut types = vec![1u8; cells];
let mut next = 0usize;
for (index, spec) in self.types.iter().enumerate() {
let Some(&fraction) = self.initial.fractions.get(&spec.name) else {
continue;
};
let share = ((fraction * cells as f64).round() as usize).min(cells - next);
for slot in types.iter_mut().skip(next).take(share) {
*slot = index as u8 + 1;
}
next += share;
}
types
}
#[must_use]
pub fn model(&self) -> Model {
let mut types = vec![CellType::default()];
types.extend(self.types.iter().map(|t| CellType {
target_volume: t.target_volume,
lambda_volume: t.lambda_volume,
target_surface: t.target_surface,
lambda_surface: t.lambda_surface,
division_volume: t.division_volume,
death_rate: t.death_rate,
target_length: t.target_length,
lambda_length: t.lambda_length,
connected: t.connected,
max_activity: t.max_activity,
lambda_activity: t.lambda_activity,
external: t.external,
}));
let species: Vec<Species> = self
.fields
.iter()
.map(|f| Species {
name: f.name.clone(),
diffusion: f.diffusion,
decay: f.decay,
initial: f.initial,
})
.collect();
let rates = |map: &BTreeMap<String, f64>| -> Vec<f64> {
species
.iter()
.map(|s| map.get(&s.name).copied().unwrap_or(0.0))
.collect()
};
let mut exchange = vec![Exchange {
secretion: vec![0.0; species.len()],
uptake: vec![0.0; species.len()],
}];
exchange.extend(self.types.iter().map(|t| Exchange {
secretion: rates(&t.secretion),
uptake: rates(&t.uptake),
}));
let mut chemotaxis = vec![vec![0.0; species.len()]];
chemotaxis.extend(self.types.iter().map(|t| rates(&t.chemotaxis)));
Model {
species,
exchange,
chemotaxis,
contact: self.effective_contact(),
width: self.width,
height: self.height,
depth: self.depth,
types,
temperature: self.temperature,
neighbour_order: self.neighbour_order,
seed: self.seed,
}
}
pub fn from_json(text: &str) -> Result<Self, String> {
let bp: Self = serde_json::from_str(text).map_err(|e| e.to_string())?;
let names: Vec<&str> = bp.types.iter().map(|t| t.name.as_str()).collect();
for name in bp.initial.fractions.keys() {
if !names.contains(&name.as_str()) {
return Err(format!(
"the initial condition names the type {name}, which the description does \
not state"
));
}
}
if let Some(adhesion) = &bp.adhesion {
let n = adhesion.molecules.len();
if adhesion.binding.len() != n * n {
return Err(format!(
"the binding matrix is {} entries for {n} molecules, want {}",
adhesion.binding.len(),
n * n
));
}
for spec in &bp.types {
for name in spec.presents.keys() {
if !adhesion.molecules.contains(name) {
return Err(format!(
"type {} presents {name}, which is not a molecule this description \
declares",
spec.name
));
}
}
}
} else {
for spec in &bp.types {
if !spec.presents.is_empty() {
return Err(format!(
"type {} presents adhesion molecules, and the description declares none",
spec.name
));
}
}
}
let known: Vec<&str> = bp.fields.iter().map(|f| f.name.as_str()).collect();
for spec in &bp.types {
for (what, map) in [
("secretion", &spec.secretion),
("uptake", &spec.uptake),
("chemotaxis", &spec.chemotaxis),
] {
for name in map.keys() {
if !known.contains(&name.as_str()) {
return Err(format!(
"type {} states {what} of {name}, which is not a field this \
description declares",
spec.name
));
}
}
}
}
bp.model().validate()?;
Ok(bp)
}
}