Skip to main content

celestial_pointing/
session.rs

1use crate::error::{Error, Result};
2use crate::model::PointingModel;
3use crate::observation::{IndatFile, MountType, Observation, SiteParams};
4use crate::solver::{self, FitResult};
5use celestial_core::Angle;
6use celestial_time::JulianDate;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum AdjustDirection {
10    #[default]
11    TelescopeToStar,
12    StarToTelescope,
13}
14
15pub struct Session {
16    pub observations: Vec<Observation>,
17    pub model: PointingModel,
18    pub site: Option<SiteParams>,
19    pub mount_type: MountType,
20    pub last_fit: Option<FitResult>,
21    pub header_lines: Vec<String>,
22    pub date: Option<JulianDate>,
23    pub adjust_direction: AdjustDirection,
24    pub lst_override: Option<Angle>,
25}
26
27impl Default for Session {
28    fn default() -> Self {
29        Self {
30            observations: Vec::new(),
31            model: PointingModel::new(),
32            site: None,
33            mount_type: MountType::GermanEquatorial,
34            last_fit: None,
35            header_lines: Vec::new(),
36            date: None,
37            adjust_direction: AdjustDirection::default(),
38            lst_override: None,
39        }
40    }
41}
42
43impl Session {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    pub fn load_indat(&mut self, indat: IndatFile) {
49        self.observations = indat.observations;
50        self.site = Some(indat.site);
51        self.mount_type = indat.mount_type;
52        self.header_lines = indat.header_lines;
53        self.date = Some(indat.date);
54        self.last_fit = None;
55    }
56
57    pub fn fit(&mut self) -> Result<&FitResult> {
58        let lat = self.latitude();
59        let active: Vec<&Observation> = self.observations.iter().filter(|o| !o.masked).collect();
60        let fixed = self.model.fixed_flags();
61        let coefficients = self.model.coefficients();
62        let result = solver::fit_model(&active, self.model.terms(), fixed, coefficients, lat)?;
63        self.model.set_coefficients(&result.coefficients)?;
64        self.last_fit = Some(result);
65        Ok(self.last_fit.as_ref().unwrap())
66    }
67
68    pub fn active_observation_count(&self) -> usize {
69        self.observations.iter().filter(|o| !o.masked).count()
70    }
71
72    pub fn masked_observation_count(&self) -> usize {
73        self.observations.iter().filter(|o| o.masked).count()
74    }
75
76    pub fn observation_count(&self) -> usize {
77        self.observations.len()
78    }
79
80    pub fn current_lst(&self) -> Result<Angle> {
81        if let Some(lst) = self.lst_override {
82            return Ok(lst);
83        }
84        Err(Error::NoLst)
85    }
86
87    pub fn latitude(&self) -> f64 {
88        self.site.as_ref().map_or(0.0, |s| s.latitude.radians())
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::observation::PierSide;
96    use crate::parser::parse_indat;
97    use celestial_core::Angle;
98
99    #[test]
100    fn new_session_defaults() {
101        let session = Session::new();
102        assert_eq!(session.observation_count(), 0);
103        assert_eq!(session.mount_type, MountType::GermanEquatorial);
104        assert!(session.site.is_none());
105        assert!(session.last_fit.is_none());
106        assert!(session.header_lines.is_empty());
107        assert!(session.date.is_none());
108        assert_eq!(session.model.term_count(), 0);
109    }
110
111    #[test]
112    fn latitude_no_site_returns_zero() {
113        let session = Session::new();
114        assert_eq!(session.latitude(), 0.0);
115    }
116
117    #[test]
118    fn load_indat_populates_session() {
119        let content = "\
120ASCOM Mount
121:NODA
122:EQUAT
123+39 00 26 2024 7 14 29.20 987.00 231.65  0.94 0.5500 0.0065
12421 43 18.4460 +72 29 08.368 09 28 59.9527 +109 20 06.469  16 23.130
12523 46 02.2988 +77 38 38.725 11 26 17.6308 +104 03 28.734  16 24.711";
126
127        let indat = parse_indat(content).unwrap();
128        let mut session = Session::new();
129        session.load_indat(indat);
130
131        assert_eq!(session.observation_count(), 2);
132        assert_eq!(session.mount_type, MountType::GermanEquatorial);
133        assert!(session.site.is_some());
134        assert!(session.date.is_some());
135        assert_eq!(session.header_lines.len(), 1);
136        assert!(session.last_fit.is_none());
137    }
138
139    #[test]
140    fn load_indat_sets_latitude() {
141        let content = "\
142!Test
143:EQUAT
144+39 00 26 2024 7 14 29.20 987.00 231.65 0.94 0.5500 0.0065
14521 43 18.4460 +72 29 08.368 09 28 59.9527 +109 20 06.469  16 23.130";
146
147        let indat = parse_indat(content).unwrap();
148        let mut session = Session::new();
149        session.load_indat(indat);
150
151        let expected = Angle::from_degrees(39.0 + 26.0 / 3600.0).radians();
152        assert_eq!(session.latitude(), expected);
153    }
154
155    #[test]
156    fn load_indat_clears_previous_fit() {
157        let content = "\
158!Test
159:EQUAT
160+39 00 26 2024 7 14 29.20 987.00 231.65 0.94 0.5500 0.0065
16121 43 18.4460 +72 29 08.368 09 28 59.9527 +109 20 06.469  16 23.130";
162
163        let indat1 = parse_indat(content).unwrap();
164        let indat2 = parse_indat(content).unwrap();
165        let mut session = Session::new();
166        session.load_indat(indat1);
167        session.last_fit = Some(FitResult {
168            coefficients: vec![1.0],
169            sigma: vec![0.1],
170            sky_rms: 5.0,
171            term_names: vec!["IH".to_string()],
172        });
173        session.load_indat(indat2);
174        assert!(session.last_fit.is_none());
175    }
176
177    fn make_obs(cmd_ha_arcsec: f64, act_ha_arcsec: f64, dec_deg: f64) -> Observation {
178        Observation {
179            catalog_ra: Angle::from_hours(0.0),
180            catalog_dec: Angle::from_degrees(dec_deg),
181            observed_ra: Angle::from_hours(0.0),
182            observed_dec: Angle::from_degrees(dec_deg),
183            lst: Angle::from_hours(0.0),
184            commanded_ha: Angle::from_arcseconds(cmd_ha_arcsec),
185            actual_ha: Angle::from_arcseconds(act_ha_arcsec),
186            pier_side: PierSide::East,
187            masked: false,
188        }
189    }
190
191    #[test]
192    fn fit_updates_model_and_stores_result() {
193        let mut session = Session::new();
194        session.observations = vec![
195            make_obs(0.0, 100.0, 30.0),
196            make_obs(0.0, 100.0, 45.0),
197            make_obs(0.0, 100.0, 60.0),
198        ];
199        session.model.add_term("IH").unwrap();
200        let result = session.fit().unwrap();
201        assert_eq!(result.term_names, vec!["IH"]);
202        assert!((result.coefficients[0] - (-100.0)).abs() < 1e-6);
203        assert!(session.last_fit.is_some());
204        assert_eq!(session.model.coefficients().len(), 1);
205    }
206
207    #[test]
208    fn fit_no_terms_returns_error() {
209        let mut session = Session::new();
210        session.observations = vec![make_obs(0.0, 100.0, 30.0)];
211        let result = session.fit();
212        assert!(result.is_err());
213    }
214
215    #[test]
216    fn fit_no_observations_returns_error() {
217        let mut session = Session::new();
218        session.model.add_term("IH").unwrap();
219        let result = session.fit();
220        assert!(result.is_err());
221    }
222}