altrios_core/train/
environment.rs

1use super::train_imports::*;
2
3#[serde_api]
4#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
5#[cfg_attr(feature = "pyo3", pyclass(module = "altrios", subclass, eq))]
6/// Container
7pub struct TemperatureTraceBuilder {
8    /// simulation elapsed time
9    pub time: Vec<si::Time>,
10    /// ambient temperature at sea level
11    pub temp_at_sea_level: Vec<si::ThermodynamicTemperature>,
12}
13
14#[pyo3_api]
15impl TemperatureTraceBuilder {
16    #[staticmethod]
17    #[pyo3(name = "from_csv_file")]
18    fn from_csv_file_py(pathstr: String) -> anyhow::Result<Self> {
19        Self::from_csv_file(&pathstr)
20    }
21
22    fn __len__(&self) -> usize {
23        self.len()
24    }
25}
26
27impl Init for TemperatureTraceBuilder {}
28impl SerdeAPI for TemperatureTraceBuilder {}
29
30impl TemperatureTraceBuilder {
31    fn empty() -> Self {
32        Self {
33            time: Vec::new(),
34            temp_at_sea_level: Vec::new(),
35        }
36    }
37
38    fn len(&self) -> usize {
39        self.time.len()
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.len() == 0
44    }
45
46    pub fn push(&mut self, tt_element: TemperatureTraceElement) {
47        self.time.push(tt_element.time);
48        self.temp_at_sea_level.push(tt_element.temp_at_sea_level);
49    }
50
51    pub fn trim(&mut self, start_idx: Option<usize>, end_idx: Option<usize>) -> anyhow::Result<()> {
52        let start_idx = start_idx.unwrap_or(0);
53        let end_idx = end_idx.unwrap_or_else(|| self.len());
54        ensure!(end_idx <= self.len(), format_dbg!(end_idx <= self.len()));
55
56        self.time = self.time[start_idx..end_idx].to_vec();
57        Ok(())
58    }
59
60    /// Load cycle from csv file
61    pub fn from_csv_file(pathstr: &str) -> Result<Self, anyhow::Error> {
62        let pathbuf = PathBuf::from(&pathstr);
63
64        // create empty temperature trace to be populated
65        let mut tt = Self::empty();
66
67        let file = File::open(pathbuf)?;
68        let mut rdr = csv::ReaderBuilder::new()
69            .has_headers(true)
70            .from_reader(file);
71        for result in rdr.deserialize() {
72            let tt_elem: TemperatureTraceElement = result?;
73            tt.push(tt_elem);
74        }
75        if tt.is_empty() {
76            bail!("Invalid TemperatureTrace file; TemperatureTrace is empty")
77        } else {
78            Ok(tt)
79        }
80    }
81}
82
83impl Default for TemperatureTraceBuilder {
84    fn default() -> Self {
85        let time_s: Vec<f64> = (0..1).map(|x| x as f64).collect();
86        let mut tt = Self {
87            time: time_s.iter().map(|t| *t * uc::S).collect(),
88            temp_at_sea_level: vec![(22.0 + 273.15) * uc::KELVIN],
89        };
90        tt.init().unwrap();
91        tt
92    }
93}
94
95/// Element of [TemperatureTrace].  Used for vec-like operations.
96#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
97pub struct TemperatureTraceElement {
98    /// simulation time
99    time: si::Time,
100    /// ambient temperature at sea level
101    pub temp_at_sea_level: si::ThermodynamicTemperature,
102}
103
104#[serde_api]
105#[derive(Clone, Debug, PartialEq)]
106#[cfg_attr(feature = "pyo3", pyclass(module = "altrios", subclass, eq))]
107/// Container for an interpolator of temperature at sea level (to be corrected for altitude)
108pub struct TemperatureTrace(pub(crate) Interp1DOwned<f64, strategy::Linear>);
109
110#[pyo3_api]
111impl TemperatureTrace {}
112
113impl Init for TemperatureTrace {}
114impl SerdeAPI for TemperatureTrace {}
115
116impl TemperatureTrace {
117    pub fn get_temp_at_time_and_elev(
118        &self,
119        time: si::Time,
120        elev: si::Length,
121    ) -> anyhow::Result<si::ThermodynamicTemperature> {
122        Ok(self.get_temp_at_elev(self.get_temp_at_time_and_sea_level(time)?, elev))
123    }
124
125    fn get_temp_at_time_and_sea_level(
126        &self,
127        time: si::Time,
128    ) -> anyhow::Result<si::ThermodynamicTemperature> {
129        Ok(self
130            .0
131            .interpolate(&[time.get::<si::second>()])
132            .map(|te| (te + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)?)
133    }
134
135    /// Source: <https://www.grc.nasa.gov/WWW/K-12/rocket/atmosmet.html>  
136    ///
137    /// # Equations used
138    /// T = 15.04 - .00649 * h  
139    fn get_temp_at_elev(
140        &self,
141        temp_at_sea_level: si::ThermodynamicTemperature,
142        elev: si::Length,
143    ) -> si::ThermodynamicTemperature {
144        (((15.04 - 0.00649 * elev.get::<si::meter>())
145            + (temp_at_sea_level.get::<si::degree_celsius>() - 15.04))
146            + uc::CELSIUS_TO_KELVIN)
147            * uc::KELVIN
148    }
149}
150
151impl Serialize for TemperatureTrace {
152    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
153    where
154        S: serde::Serializer,
155    {
156        let builder: TemperatureTraceBuilder = TemperatureTraceBuilder::try_from(self.clone())
157            .map_err(|e| serde::ser::Error::custom(format!("{:?}", e)))?;
158        builder.serialize(serializer)
159    }
160}
161
162impl<'de> Deserialize<'de> for TemperatureTrace {
163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164    where
165        D: serde::Deserializer<'de>,
166    {
167        let value: TemperatureTraceBuilder = TemperatureTraceBuilder::deserialize(deserializer)?;
168        let tt: Self =
169            Self::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?;
170        Ok(tt)
171    }
172}
173
174impl TryFrom<TemperatureTraceBuilder> for TemperatureTrace {
175    type Error = anyhow::Error;
176    fn try_from(value: TemperatureTraceBuilder) -> anyhow::Result<Self> {
177        Ok(Self(Interp1D::new(
178            value.time.iter().map(|t| t.get::<si::second>()).collect(),
179            value
180                .temp_at_sea_level
181                .iter()
182                .map(|te| te.get::<si::degree_celsius>())
183                .collect(),
184            strategy::Linear,
185            Extrapolate::Clamp,
186        )?))
187    }
188}
189
190impl TryFrom<TemperatureTrace> for TemperatureTraceBuilder {
191    type Error = anyhow::Error;
192    fn try_from(value: TemperatureTrace) -> anyhow::Result<Self> {
193        Ok(Self {
194            time: value.0.data.grid[0].iter().map(|x| *x * uc::S).collect(),
195            temp_at_sea_level: value
196                .0
197                .data
198                .values
199                .iter()
200                .map(|y| (*y + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
201                .collect(),
202        })
203    }
204}
205
206impl Default for TemperatureTrace {
207    fn default() -> Self {
208        Self::try_from(TemperatureTraceBuilder::default()).unwrap()
209    }
210}
211
212impl TemperatureTrace {}