Skip to main content

gapsmith_core/
reaction.rs

1//! Reaction metadata.
2//!
3//! The stoichiometry itself lives in the model-level `StoichMatrix`; this
4//! struct carries only per-reaction attributes that the solver and output
5//! writers consume.
6
7use crate::RxnId;
8use serde::{Deserialize, Serialize};
9
10/// SEED reactions carry a curation status used by gapseq to filter the
11/// candidate pool. See `src/correct_seed_rxnDB.R`.
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SeedStatus {
15    Approved,
16    Corrected,
17    NotAssessed,
18    Removed,
19    /// Status not recorded (synthetic reactions: exchange, demand, biomass).
20    #[default]
21    None,
22}
23
24impl SeedStatus {
25    pub fn is_usable(self) -> bool {
26        matches!(self, SeedStatus::Approved | SeedStatus::Corrected)
27    }
28}
29
30/// Reversibility marker.
31///
32/// Matches the `>` / `<` / `=` codes used throughout `dat/seed_reactions_corrected.tsv`.
33#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum Reversibility {
36    Forward,
37    Backward,
38    Reversible,
39}
40
41impl Reversibility {
42    pub fn from_code(c: char) -> Option<Self> {
43        match c {
44            '>' => Some(Self::Forward),
45            '<' => Some(Self::Backward),
46            '=' => Some(Self::Reversible),
47            _ => None,
48        }
49    }
50
51    pub fn code(self) -> char {
52        match self {
53            Self::Forward => '>',
54            Self::Backward => '<',
55            Self::Reversible => '=',
56        }
57    }
58}
59
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct Reaction {
62    pub id: RxnId,
63    pub name: String,
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub ec: Vec<String>,
66    pub lb: f64,
67    pub ub: f64,
68    #[serde(default)]
69    pub obj_coef: f64,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub gpr_raw: Option<String>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub subsystem: Option<String>,
74    #[serde(default)]
75    pub seed_status: SeedStatus,
76    #[serde(default)]
77    pub is_exchange: bool,
78    #[serde(default)]
79    pub is_biomass: bool,
80    /// gapseq gs.origin code (see `generate_GSdraft.R`): 0 high-evidence SEED,
81    /// 5 conditional transporter, 6 biomass, 7 exchange, 8 diffusion, 9 pathway-only.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub gs_origin: Option<i8>,
84    /// Bitscore from homology search (NaN / None if absent).
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub bitscore: Option<f32>,
87    /// pFBA weight (populated by the gap-filler).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub weight: Option<f32>,
90}
91
92impl Reaction {
93    pub fn new(id: impl Into<RxnId>, name: impl Into<String>, lb: f64, ub: f64) -> Self {
94        Self {
95            id: id.into(),
96            name: name.into(),
97            ec: Vec::new(),
98            lb,
99            ub,
100            obj_coef: 0.0,
101            gpr_raw: None,
102            subsystem: None,
103            seed_status: SeedStatus::None,
104            is_exchange: false,
105            is_biomass: false,
106            gs_origin: None,
107            bitscore: None,
108            weight: None,
109        }
110    }
111
112    pub fn reversibility(&self) -> Reversibility {
113        match (self.lb < 0.0, self.ub > 0.0) {
114            (true, true) => Reversibility::Reversible,
115            (false, true) => Reversibility::Forward,
116            (true, false) => Reversibility::Backward,
117            // lb == 0 and ub == 0: treat as forward-blocked.
118            (false, false) => Reversibility::Forward,
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn reversibility_roundtrip() {
129        for c in ['>', '<', '='] {
130            let r = Reversibility::from_code(c).unwrap();
131            assert_eq!(r.code(), c);
132        }
133        assert!(Reversibility::from_code('?').is_none());
134    }
135
136    #[test]
137    fn reversibility_from_bounds() {
138        assert_eq!(Reaction::new("r", "r", -1000.0, 1000.0).reversibility(), Reversibility::Reversible);
139        assert_eq!(Reaction::new("r", "r", 0.0, 1000.0).reversibility(), Reversibility::Forward);
140        assert_eq!(Reaction::new("r", "r", -1000.0, 0.0).reversibility(), Reversibility::Backward);
141    }
142}