1use astrors_fork::io::hdus::{
2 image::{imagehdu::ImageHDU, ImageData},
3 primaryhdu::PrimaryHDU,
4};
5use ndarray::{ArrayBase, Axis, Dim, IxDynImpl, OwnedRepr};
6use polars::export::chrono::NaiveDateTime;
7use polars::prelude::*;
8use std::ops::Mul;
9
10use crate::enums::HeaderValue;
11use crate::errors::FitsLoaderError;
12
13pub fn theta_offset(theta: f64, ccd_theta: f64) -> f64 {
15 let ccd_theta = ccd_theta / 2.0;
17 ccd_theta - theta
18}
19
20pub fn q(lam: f64, theta: f64, angle_offset: f64) -> f64 {
21 let theta = theta - angle_offset;
22 match 4.0 * std::f64::consts::PI * theta.to_radians().sin() / lam {
23 q if q < 0.0 => 0.0,
24 q => q,
25 }
26}
27
28pub fn col_from_array(
29 name: PlSmallStr,
30 array: ArrayBase<OwnedRepr<i64>, Dim<IxDynImpl>>,
31) -> Result<Column, PolarsError> {
32 let size0 = array.len_of(Axis(0));
33 let size1 = array.len_of(Axis(1));
34
35 let mut s = Column::new_empty(
36 name,
37 &DataType::List(Box::new(DataType::List(Box::new(DataType::Int64)))),
38 );
39
40 let mut chunked_builder = ListPrimitiveChunkedBuilder::<Int64Type>::new(
41 PlSmallStr::EMPTY,
42 array.len_of(Axis(0)),
43 array.len_of(Axis(1)) * array.len_of(Axis(0)),
44 DataType::Int64,
45 );
46 for row in array.axis_iter(Axis(0)) {
47 match row.as_slice() {
48 Some(row) => chunked_builder.append_slice(row),
49 None => chunked_builder.append_slice(&row.to_owned().into_raw_vec()),
50 }
51 }
52 let new_series = chunked_builder
53 .finish()
54 .into_series()
55 .implode()
56 .unwrap()
57 .into_column();
58 let _ = s.extend(&new_series);
59 let s = s.cast(&DataType::Array(
60 Box::new(DataType::Array(Box::new(DataType::Int32), size1)),
61 size0,
62 ));
63 s
64}
65pub fn add_calculated_domains(lzf: LazyFrame) -> DataFrame {
67 let h = physical_constants::PLANCK_CONSTANT_IN_EV_PER_HZ;
68 let c = physical_constants::SPEED_OF_LIGHT_IN_VACUUM * 1e10;
69 let lz = lzf
71 .sort(["Sample Name"], Default::default())
72 .sort(["Scan ID"], Default::default())
73 .sort(["Frame Number"], Default::default())
74 .with_columns(&[
75 col("EXPOSURE [s]").round(3).alias("EXPOSURE [s]"),
76 col("Higher Order Suppressor [mm]")
77 .round(2)
78 .alias("Higher Order Suppressor [mm]"),
79 col("Horizontal Exit Slit Size [um]")
80 .round(1)
81 .alias("Horizontal Exit Slit Size [um]"),
82 col("Beamline Energy [eV]")
83 .round(1)
84 .alias("Beamline Energy [eV]"),
85 ])
86 .with_column(
87 col("Beamline Energy [eV]")
88 .pow(-1)
89 .mul(lit(h * c))
90 .alias("Lambda [Å]"),
91 );
92
93 let angle_offset = lz
94 .clone()
95 .filter(col("Sample Theta [deg]").eq(0.0))
96 .last()
97 .select(&[col("CCD Theta [deg]"), col("Sample Theta [deg]")])
98 .with_column(
99 as_struct(vec![col("Sample Theta [deg]"), col("CCD Theta [deg]")])
100 .map(
101 move |s| {
102 let struc = s.struct_()?;
103 let th_series = struc.field_by_name("Sample Theta [deg]")?;
104 let theta = th_series.f64()?;
105 let ccd_th_series = struc.field_by_name("CCD Theta [deg]")?;
106 let ccd_theta = ccd_th_series.f64()?;
107 let out: Float64Chunked = theta
108 .into_iter()
109 .zip(ccd_theta.iter())
110 .map(|(theta, ccd_theta)| match (theta, ccd_theta) {
111 (Some(theta), Some(ccd_theta)) => {
112 Some(theta_offset(theta, ccd_theta))
113 }
114 _ => Some(0.0),
115 })
116 .collect();
117 Ok(Some(out.into_column()))
118 },
119 GetOutput::from_type(DataType::Float64),
120 )
121 .alias("Theta Offset [deg]"),
122 )
123 .select(&[col("Theta Offset [deg]")])
124 .collect()
125 .unwrap()
126 .get(0)
127 .unwrap()[0]
128 .try_extract::<f64>()
129 .unwrap();
130 let lz = lz
132 .with_column(lit(angle_offset).alias("Theta Offset [deg]"))
133 .with_column(
134 as_struct(vec![col("Sample Theta [deg]"), col("Lambda [Å]")])
135 .map(
136 move |s| {
137 let struc = s.struct_()?;
138 let th_series = struc.field_by_name("Sample Theta [deg]")?;
139 let theta = th_series.f64()?;
140 let lam_series = struc.field_by_name("Lambda [Å]")?;
141 let lam = lam_series.f64()?;
142
143 let out: Float64Chunked = theta
144 .into_iter()
145 .zip(lam.iter())
146 .map(|(theta, lam)| match (theta, lam) {
147 (Some(theta), Some(lam)) => Some(q(lam, theta, angle_offset)),
148 _ => None,
149 })
150 .collect();
151
152 Ok(Some(out.into_column()))
153 },
154 GetOutput::from_type(DataType::Float64),
155 )
156 .alias("Q [Å⁻¹]"),
157 );
158 lz.collect().unwrap()
159}
160
161pub fn process_image(img: &ImageHDU) -> Result<Vec<Column>, FitsLoaderError> {
164 let bzero = img
165 .header
166 .get_card("BZERO")
167 .ok_or_else(|| FitsLoaderError::MissingHeaderKey("BZERO".into()))?
168 .value
169 .as_int()
170 .ok_or_else(|| FitsLoaderError::FitsError("BZERO not an integer".into()))?;
171
172 match &img.data {
173 ImageData::I16(image) => {
174 let data = image.map(|&x| i64::from(x as i64 + bzero));
175 Ok(vec![col_from_array("Raw".into(), data.clone()).unwrap()])
176 }
177 _ => Err(FitsLoaderError::UnsupportedImageData),
178 }
179}
180
181pub fn process_metadata(
182 hdu: &PrimaryHDU,
183 keys: &Vec<HeaderValue>,
184) -> Result<Vec<Column>, FitsLoaderError> {
185 if keys.is_empty() {
186 Ok(hdu
187 .header
188 .iter()
189 .map(|card| {
190 let name = card.keyword.as_str();
191 let value = card.value.as_float().unwrap_or(0.0);
192 Column::new(name.into(), &[value])
193 })
194 .collect())
195 } else {
196 keys.iter()
197 .map(|key| {
198 let val = hdu
199 .header
200 .get_card(key.hdu())
201 .ok_or_else(|| FitsLoaderError::MissingHeaderKey(key.hdu().to_string()))?
202 .value
203 .as_float()
204 .ok_or_else(|| {
205 FitsLoaderError::Other(format!("Value for {} is not a float", key.hdu()))
206 })?;
207 Ok(Column::new(key.name().into(), &[val]))
208 })
209 .collect()
210 }
211}
212
213pub fn process_file_name(path: std::path::PathBuf) -> Vec<Column> {
214 let ts = path
215 .metadata()
216 .unwrap()
217 .modified()
218 .unwrap()
219 .duration_since(std::time::UNIX_EPOCH)
220 .unwrap()
221 .as_secs();
222
223 let ts = NaiveDateTime::from_timestamp(ts as i64, 0);
224
225 let file_name = path.file_stem().unwrap().to_str().unwrap_or("");
226
227 let filename_segments = file_name.split('-');
228
229 let frame = filename_segments
230 .clone()
231 .last()
232 .unwrap()
233 .parse::<u32>()
234 .unwrap_or(0);
235
236 let remaining = filename_segments
237 .clone()
238 .take(filename_segments.clone().count() - 1)
239 .collect::<String>();
240
241 if remaining.is_empty() {
242 return vec![
243 Column::new("Scan ID".into(), vec![0]),
244 Column::new("Sample Name".into(), vec![""]),
245 Column::new("Frame Number".into(), vec![0]),
246 Column::new("Timestamp".into(), vec![ts]),
247 ];
248 }
249
250 let scan_id = remaining
251 .chars()
252 .rev()
253 .take(5)
254 .collect::<String>()
255 .chars()
256 .rev()
257 .collect::<String>();
258
259 let sample_name = remaining
260 .chars()
261 .take(remaining.len() - 5)
262 .collect::<String>()
263 .trim_end_matches(|c| c == '-' || c == '_')
264 .to_string();
265
266 vec![
267 Column::new("File Name".into(), vec![remaining]),
268 Column::new("Scan ID".into(), vec![scan_id]),
269 Column::new("Sample Name".into(), vec![sample_name]),
270 Column::new("Frame Number".into(), vec![frame]),
271 Column::new("Timestamp".into(), vec![ts]),
272 ]
273}