celestial_pointing/
session.rs1use 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}