1use crate::RxnId;
8use serde::{Deserialize, Serialize};
9
10#[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 #[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub gs_origin: Option<i8>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub bitscore: Option<f32>,
87 #[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 (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}