use crate::{Compartment, CpdId, GeneId, Metabolite, Reaction, RxnId, StoichMatrix};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ModelAnnot {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gapsmith_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seqdb_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tax_domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gram: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ModelError {
#[error("unknown reaction id `{0}`")]
UnknownRxn(String),
#[error("unknown metabolite id `{0}`")]
UnknownMet(String),
#[error("duplicate reaction id `{0}`")]
DuplicateRxn(String),
#[error("duplicate metabolite id `{0}`")]
DuplicateMet(String),
#[error(
"stoichiometric matrix shape {got:?} does not match model dimensions {expected:?}"
)]
ShapeMismatch { got: (usize, usize), expected: (usize, usize) },
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Model {
pub annot: ModelAnnot,
pub compartments: Vec<Compartment>,
pub mets: Vec<Metabolite>,
pub rxns: Vec<Reaction>,
#[serde(default)]
pub genes: Vec<GeneId>,
pub s: StoichMatrix,
}
impl Model {
pub fn new(id: impl Into<String>) -> Self {
Self {
annot: ModelAnnot { id: id.into(), ..Default::default() },
compartments: Compartment::default_three(),
mets: Vec::new(),
rxns: Vec::new(),
genes: Vec::new(),
s: StoichMatrix::zeros(0, 0),
}
}
pub fn rxn_count(&self) -> usize {
self.rxns.len()
}
pub fn met_count(&self) -> usize {
self.mets.len()
}
pub fn rxn_index(&self) -> HashMap<RxnId, usize> {
self.rxns
.iter()
.enumerate()
.map(|(i, r)| (r.id.clone(), i))
.collect()
}
pub fn met_index(&self) -> HashMap<CpdId, usize> {
self.mets
.iter()
.enumerate()
.map(|(i, m)| (m.id.clone(), i))
.collect()
}
pub fn check_shape(&self) -> Result<(), ModelError> {
let expected = (self.mets.len(), self.rxns.len());
let got = (self.s.rows(), self.s.cols());
if got == expected {
Ok(())
} else {
Err(ModelError::ShapeMismatch { got, expected })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CompartmentId, Reversibility};
fn toy_model() -> Model {
let mut m = Model::new("toy");
m.mets.push(Metabolite::new("cpdA", "A", CompartmentId::CYTOSOL));
m.mets.push(Metabolite::new("cpdB", "B", CompartmentId::CYTOSOL));
m.mets.push(Metabolite::new("cpdC", "C", CompartmentId::CYTOSOL));
let mut r1 = Reaction::new("r1", "A -> B", 0.0, 1000.0);
r1.obj_coef = 0.0;
m.rxns.push(r1);
m.rxns.push(Reaction::new("r2", "B -> C", 0.0, 1000.0));
m.s = StoichMatrix::from_triplets(
3,
2,
vec![(0, 0, -1.0), (1, 0, 1.0), (1, 1, -1.0), (2, 1, 1.0)],
);
m
}
#[test]
fn shape_check() {
let m = toy_model();
m.check_shape().unwrap();
}
#[test]
fn bad_shape_detected() {
let mut m = toy_model();
m.rxns.push(Reaction::new("r3", "C -> sink", 0.0, 1000.0));
assert!(m.check_shape().is_err());
}
#[test]
fn reaction_indexing() {
let m = toy_model();
let idx = m.rxn_index();
assert_eq!(idx[&RxnId::new("r1")], 0);
assert_eq!(idx[&RxnId::new("r2")], 1);
}
#[test]
fn reversibility_from_bounds() {
let m = toy_model();
assert_eq!(m.rxns[0].reversibility(), Reversibility::Forward);
}
}