1use std::path::{Path, PathBuf};
9
10use fitsio::FitsFile;
11use ndarray::{ArrayD, Axis, IxDyn};
12
13use crate::error::{FitsCubeError, Result};
14use crate::fits_io::{
15 PixelType, copy_header_only, delete_key, resize_image, update_key_f64, update_key_i64,
16 update_key_str,
17};
18
19#[derive(Debug, Clone, Default)]
21pub struct ExtractOptions {
22 pub hdu_index: usize,
23 pub channel_index: Option<usize>,
24 pub time_index: Option<usize>,
25 pub overwrite: bool,
26 pub output_path: Option<PathBuf>,
27}
28
29struct TargetIndex {
31 axis_name: &'static str,
32 axis_index: usize,
33 output_name: &'static str,
34}
35
36fn create_target_index(opts: &ExtractOptions) -> Result<TargetIndex> {
38 match (opts.channel_index, opts.time_index) {
39 (Some(_), Some(_)) => Err(FitsCubeError::InvalidSpec(
40 "Both time and channel index are set. Only one may be set.".to_string(),
41 )),
42 (None, None) => Err(FitsCubeError::InvalidSpec(
43 "Both channel index and time index are None. One needs to be set.".to_string(),
44 )),
45 (Some(c), None) => Ok(TargetIndex {
46 axis_name: "FREQ",
47 axis_index: c,
48 output_name: "channel",
49 }),
50 (None, Some(t)) => Ok(TargetIndex {
51 axis_name: "TIME",
52 axis_index: t,
53 output_name: "timestep",
54 }),
55 }
56}
57
58fn default_output_path(input: &Path, target: &TargetIndex) -> PathBuf {
61 let parent = input.parent().unwrap_or_else(|| Path::new("."));
62 let stem = input.file_stem().unwrap_or_default().to_string_lossy();
63 let ext = input.extension().unwrap_or_default().to_string_lossy();
64 parent.join(format!(
65 "{stem}.{}-{}.{ext}",
66 target.output_name, target.axis_index
67 ))
68}
69
70struct AxisWcs {
72 fits_idx: usize,
73 ctype: String,
74 crval: f64,
75 cdelt: f64,
76 cunit: Option<String>,
77}
78
79fn find_axis(fptr: &mut FitsFile, hdu_index: usize, name: &str) -> Result<AxisWcs> {
80 let hdu = fptr.hdu(hdu_index)?;
81 let naxis: i64 = hdu.read_key(fptr, "NAXIS")?;
82 for axis in 1..=naxis {
83 let ctype: String = match hdu.read_key(fptr, &format!("CTYPE{axis}")) {
84 Ok(c) => c,
85 Err(_) => continue,
86 };
87 if ctype.contains(name) {
88 let crval: f64 = hdu.read_key(fptr, &format!("CRVAL{axis}")).unwrap_or(0.0);
89 let cdelt: f64 = hdu.read_key(fptr, &format!("CDELT{axis}")).unwrap_or(1.0);
90 let cunit: Option<String> = hdu.read_key(fptr, &format!("CUNIT{axis}")).ok();
91 return Ok(AxisWcs {
92 fits_idx: axis as usize,
93 ctype,
94 crval,
95 cdelt,
96 cunit,
97 });
98 }
99 }
100 Err(FitsCubeError::TargetAxisMissing(format!(
101 "Did not find the {name} axis"
102 )))
103}
104
105fn contains_beam_table(fptr: &mut FitsFile, hdu_index: usize) -> bool {
107 let Ok(hdu) = fptr.hdu(hdu_index) else {
108 return false;
109 };
110 if let Ok(b) = hdu.read_key::<bool>(fptr, "CASAMBM") {
111 return b;
112 }
113 if let Ok(s) = hdu.read_key::<String>(fptr, "CASAMBM") {
114 return matches!(s.trim(), "T" | "TRUE");
115 }
116 false
117}
118
119fn read_geom(fptr: &mut FitsFile, hdu_index: usize) -> Result<(Vec<usize>, i64)> {
121 let hdu = fptr.hdu(hdu_index)?;
122 let naxis: i64 = hdu.read_key(fptr, "NAXIS")?;
123 let bitpix: i64 = hdu.read_key(fptr, "BITPIX")?;
124 let mut dims = Vec::with_capacity(naxis as usize);
125 for i in 1..=naxis {
126 let n: i64 = hdu.read_key(fptr, &format!("NAXIS{i}"))?;
127 dims.push(n as usize);
128 }
129 Ok((dims, bitpix))
130}
131
132struct BeamRow {
134 bmaj_deg: f64,
135 bmin_deg: f64,
136 bpa_deg: f64,
137}
138
139fn extract_beam_row(path: &Path, index: usize) -> Result<BeamRow> {
140 let mut fptr = FitsFile::open(path.to_string_lossy().as_ref())?;
141 let hdu = fptr
142 .hdu("BEAMS")
143 .map_err(|_| FitsCubeError::Other("Beam table was not found".to_string()))?;
144 let bmaj: Vec<f32> = hdu.read_col(&mut fptr, "BMAJ")?;
145 let bmin: Vec<f32> = hdu.read_col(&mut fptr, "BMIN")?;
146 let bpa: Vec<f32> = hdu.read_col(&mut fptr, "BPA")?;
147 if index >= bmaj.len() {
148 return Err(FitsCubeError::ChannelMissing(format!(
149 "beam table has {} rows, requested index {index}",
150 bmaj.len()
151 )));
152 }
153 Ok(BeamRow {
155 bmaj_deg: bmaj[index] as f64 / 3600.0,
156 bmin_deg: bmin[index] as f64 / 3600.0,
157 bpa_deg: bpa[index] as f64,
158 })
159}
160
161fn take_plane<T: Clone + 'static>(
164 flat: Vec<T>,
165 dims_fits: &[usize],
166 array_idx: usize,
167 axis_index: usize,
168) -> Result<Vec<T>> {
169 let shape_np: Vec<usize> = dims_fits.iter().rev().copied().collect();
171 let arr = ArrayD::from_shape_vec(IxDyn(&shape_np), flat)?;
172 let plane = arr.index_axis(Axis(array_idx), axis_index); let plane = plane.insert_axis(Axis(array_idx)); let std = plane.as_standard_layout();
175 Ok(std.iter().cloned().collect())
176}
177
178#[allow(clippy::too_many_arguments)]
179fn write_plane<T: CubeImage>(
180 out_path: &Path,
181 cube: &Path,
182 hdu_index: usize,
183 dims_out_fits: &[usize],
184 bitpix: i64,
185 plane: &[T],
186 axis: &AxisWcs,
187 axis_index: usize,
188 beam: Option<&BeamRow>,
189 drop_casambm: bool,
190 overwrite: bool,
191) -> Result<()> {
192 if out_path.exists() {
193 if overwrite {
194 std::fs::remove_file(out_path)?;
195 } else {
196 return Err(FitsCubeError::OutputExists(out_path.to_path_buf()));
197 }
198 }
199 copy_header_only_hdu(cube, out_path, hdu_index)?;
201 let mut fptr = FitsFile::edit(out_path.to_string_lossy().as_ref())?;
202 fptr.primary_hdu()?;
203 resize_image(&mut fptr, bitpix, dims_out_fits)?;
204
205 let fi = axis.fits_idx;
206 let new_crval = axis.crval + (axis_index as f64) * axis.cdelt;
207 update_key_str(&mut fptr, &format!("CTYPE{fi}"), &axis.ctype)?;
208 update_key_i64(&mut fptr, &format!("CRPIX{fi}"), 1)?;
209 update_key_f64(&mut fptr, &format!("CRVAL{fi}"), new_crval)?;
210 update_key_f64(&mut fptr, &format!("CDELT{fi}"), axis.cdelt)?;
211 if let Some(cunit) = &axis.cunit {
212 update_key_str(&mut fptr, &format!("CUNIT{fi}"), cunit)?;
213 }
214
215 if let Some(b) = beam {
216 update_key_f64(&mut fptr, "BMAJ", b.bmaj_deg)?;
217 update_key_f64(&mut fptr, "BMIN", b.bmin_deg)?;
218 update_key_f64(&mut fptr, "BPA", b.bpa_deg)?;
219 }
220 if drop_casambm {
221 delete_key(&mut fptr, "CASAMBM")?;
222 }
223
224 T::write_primary(&mut fptr, plane)?;
225 Ok(())
226}
227
228fn copy_header_only_hdu(input: &Path, output: &Path, hdu_index: usize) -> Result<()> {
230 if hdu_index == 0 {
231 return Ok(copy_header_only(input, output)?);
232 }
233 Err(FitsCubeError::Other(format!(
237 "extracting from non-primary HDU index {hdu_index} is not supported"
238 )))
239}
240
241trait CubeImage: Clone + ndarray::ScalarOperand + 'static {
243 fn read_primary(fptr: &mut FitsFile, hdu_index: usize) -> Result<Vec<Self>>;
244 fn write_primary(fptr: &mut FitsFile, data: &[Self]) -> Result<()>;
245}
246
247macro_rules! impl_cube_image {
248 ($t:ty) => {
249 impl CubeImage for $t {
250 fn read_primary(fptr: &mut FitsFile, hdu_index: usize) -> Result<Vec<Self>> {
251 let hdu = fptr.hdu(hdu_index)?;
252 Ok(hdu.read_image(fptr)?)
253 }
254 fn write_primary(fptr: &mut FitsFile, data: &[Self]) -> Result<()> {
255 let hdu = fptr.primary_hdu()?;
256 hdu.write_image(fptr, data)?;
257 Ok(())
258 }
259 }
260 };
261}
262impl_cube_image!(f32);
263impl_cube_image!(f64);
264
265#[allow(clippy::too_many_arguments)]
266fn extract_typed<T: CubeImage>(
267 cube: &Path,
268 opts: &ExtractOptions,
269 target: &TargetIndex,
270 out_path: &Path,
271 dims: &[usize],
272 bitpix: i64,
273 axis: AxisWcs,
274 beam: Option<BeamRow>,
275 drop_casambm: bool,
276) -> Result<()> {
277 let array_idx = dims.len() - axis.fits_idx; if target.axis_index >= dims[array_idx] {
279 return Err(FitsCubeError::ChannelMissing(format!(
280 "index {} outside cube axis of length {}",
281 target.axis_index, dims[array_idx]
282 )));
283 }
284
285 let mut fptr = FitsFile::open(cube.to_string_lossy().as_ref())?;
286 let flat: Vec<T> = T::read_primary(&mut fptr, opts.hdu_index)?;
287 drop(fptr);
288
289 let plane = take_plane(flat, dims, array_idx, target.axis_index)?;
290
291 let mut dims_out = dims.to_vec();
292 dims_out[axis.fits_idx - 1] = 1;
293
294 write_plane::<T>(
295 out_path,
296 cube,
297 opts.hdu_index,
298 &dims_out,
299 bitpix,
300 &plane,
301 &axis,
302 target.axis_index,
303 beam.as_ref(),
304 drop_casambm,
305 opts.overwrite,
306 )
307}
308
309pub fn extract_plane_from_cube(cube: &Path, opts: &ExtractOptions) -> Result<PathBuf> {
311 let target = create_target_index(opts)?;
312 let out_path = opts
313 .output_path
314 .clone()
315 .unwrap_or_else(|| default_output_path(cube, &target));
316
317 let mut fptr = FitsFile::open(cube.to_string_lossy().as_ref())?;
318 let axis = find_axis(&mut fptr, opts.hdu_index, target.axis_name)?;
319 let (dims, bitpix) = read_geom(&mut fptr, opts.hdu_index)?;
320 let has_beam_table = contains_beam_table(&mut fptr, opts.hdu_index);
321 drop(fptr);
322
323 let beam = if has_beam_table {
324 extract_beam_row(cube, target.axis_index).ok()
325 } else {
326 None
327 };
328 let drop_casambm = has_beam_table;
329
330 match PixelType::from_bitpix(bitpix) {
331 PixelType::F32 => extract_typed::<f32>(
332 cube,
333 opts,
334 &target,
335 &out_path,
336 &dims,
337 bitpix,
338 axis,
339 beam,
340 drop_casambm,
341 )?,
342 PixelType::F64 => extract_typed::<f64>(
343 cube,
344 opts,
345 &target,
346 &out_path,
347 &dims,
348 bitpix,
349 axis,
350 beam,
351 drop_casambm,
352 )?,
353 }
354 Ok(out_path)
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn output_path_default() {
363 let target = TargetIndex {
364 axis_name: "FREQ",
365 axis_index: 5,
366 output_name: "channel",
367 };
368 let p = default_output_path(Path::new("/data/cube.fits"), &target);
369 assert_eq!(p, PathBuf::from("/data/cube.channel-5.fits"));
370 }
371
372 #[test]
373 fn both_indices_errors() {
374 let opts = ExtractOptions {
375 channel_index: Some(1),
376 time_index: Some(2),
377 ..Default::default()
378 };
379 assert!(create_target_index(&opts).is_err());
380 }
381
382 #[test]
383 fn neither_index_errors() {
384 let opts = ExtractOptions::default();
385 assert!(create_target_index(&opts).is_err());
386 }
387
388 #[test]
389 fn take_middle_plane_of_cube() {
390 let dims_fits = vec![2usize, 2, 3];
393 let mut flat = vec![0.0f64; 12];
394 for c in 0..3 {
396 for r in 0..2 {
397 for col in 0..2 {
398 flat[(c * 2 + r) * 2 + col] = c as f64;
399 }
400 }
401 }
402 let plane = take_plane(flat, &dims_fits, 0, 1).unwrap();
404 assert_eq!(plane, vec![1.0; 4]);
405 }
406}