1use ndarray::Array2;
3#[cfg(feature = "python")]
4use ndarray::ArrayView2;
5use thiserror::Error;
6
7use crate::beam::Beam;
8use crate::convolve_uv::{
9 ConvolveError, FftFloat, FftPlans, OutputGain, PlaneInput, convolve_plane,
10};
11
12#[derive(Debug, Error)]
13pub enum SmoothError {
14 #[error("convolution failed: {0}")]
15 Convolve(#[from] ConvolveError),
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum BrightnessUnit {
22 #[default]
25 JyPerBeam,
26 Kelvin,
29}
30
31impl BrightnessUnit {
32 pub fn parse(bunit: &str) -> Option<Self> {
45 let u = bunit.trim().trim_matches('\'').trim().to_ascii_uppercase();
46 match u.as_str() {
47 "K" | "KELVIN" => Some(BrightnessUnit::Kelvin),
48 "JY/BEAM" | "JY BEAM-1" | "JY/BM" | "JYBEAM" => Some(BrightnessUnit::JyPerBeam),
49 _ => None,
50 }
51 }
52
53 pub fn from_bunit(bunit: &str) -> Self {
58 match Self::parse(bunit) {
59 Some(unit) => unit,
60 None => {
61 tracing::warn!(
62 "Could not determine brightness unit from BUNIT={bunit:?}; \
63 assuming Jy/beam (flux scaling applied). Pass a recognised \
64 unit (e.g. 'Jy/beam' or 'K') to silence this warning."
65 );
66 BrightnessUnit::JyPerBeam
67 }
68 }
69 }
70}
71
72pub fn smooth<T: FftFloat>(
103 image: &Array2<T>,
104 old_beam: &Beam,
105 new_beam: &Beam,
106 dx_deg: f64,
107 dy_deg: f64,
108 cutoff_arcsec: Option<f64>,
109 unit: BrightnessUnit,
110) -> Result<Array2<T>, SmoothError> {
111 let (nrows, ncols) = image.dim();
112 let plans = FftPlans::<T>::new(nrows, ncols);
113 smooth_with_plans(
114 image,
115 old_beam,
116 new_beam,
117 dx_deg,
118 dy_deg,
119 cutoff_arcsec,
120 unit,
121 &plans,
122 )
123}
124
125#[allow(clippy::too_many_arguments)]
129pub fn smooth_with_plans<T: FftFloat>(
130 image: &Array2<T>,
131 old_beam: &Beam,
132 new_beam: &Beam,
133 dx_deg: f64,
134 dy_deg: f64,
135 cutoff_arcsec: Option<f64>,
136 unit: BrightnessUnit,
137 plans: &FftPlans<T>,
138) -> Result<Array2<T>, SmoothError> {
139 smooth_plane(
140 PlaneInput::Borrowed(image.view()),
141 old_beam,
142 new_beam,
143 dx_deg,
144 dy_deg,
145 cutoff_arcsec,
146 unit,
147 plans,
148 )
149}
150
151#[allow(clippy::too_many_arguments)]
156pub fn smooth_owned_with_plans<T: FftFloat>(
157 image: Array2<T>,
158 old_beam: &Beam,
159 new_beam: &Beam,
160 dx_deg: f64,
161 dy_deg: f64,
162 cutoff_arcsec: Option<f64>,
163 unit: BrightnessUnit,
164 plans: &FftPlans<T>,
165) -> Result<Array2<T>, SmoothError> {
166 smooth_plane(
167 PlaneInput::Owned(image),
168 old_beam,
169 new_beam,
170 dx_deg,
171 dy_deg,
172 cutoff_arcsec,
173 unit,
174 plans,
175 )
176}
177
178#[cfg(feature = "python")]
180#[allow(clippy::too_many_arguments)]
181pub(crate) fn smooth_view<T: FftFloat>(
182 image: ArrayView2<'_, T>,
183 old_beam: &Beam,
184 new_beam: &Beam,
185 dx_deg: f64,
186 dy_deg: f64,
187 cutoff_arcsec: Option<f64>,
188 unit: BrightnessUnit,
189) -> Result<Array2<T>, SmoothError> {
190 let (nrows, ncols) = image.dim();
191 let plans = FftPlans::<T>::new(nrows, ncols);
192 smooth_plane(
193 PlaneInput::Borrowed(image),
194 old_beam,
195 new_beam,
196 dx_deg,
197 dy_deg,
198 cutoff_arcsec,
199 unit,
200 &plans,
201 )
202}
203
204#[allow(clippy::too_many_arguments)]
205fn smooth_plane<T: FftFloat>(
206 image: PlaneInput<'_, T>,
207 old_beam: &Beam,
208 new_beam: &Beam,
209 dx_deg: f64,
210 dy_deg: f64,
211 cutoff_arcsec: Option<f64>,
212 unit: BrightnessUnit,
213 plans: &FftPlans<T>,
214) -> Result<Array2<T>, SmoothError> {
215 let gain = match unit {
221 BrightnessUnit::JyPerBeam => OutputGain::Ratio,
222 BrightnessUnit::Kelvin => OutputGain::InverseRatio,
223 };
224 let result = convolve_plane(
225 image,
226 old_beam,
227 new_beam,
228 dx_deg,
229 dy_deg,
230 cutoff_arcsec,
231 gain,
232 plans,
233 )?;
234 Ok(result.image)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use ndarray::Array2;
241
242 #[test]
243 fn test_parse_recognised() {
244 assert_eq!(BrightnessUnit::parse("K"), Some(BrightnessUnit::Kelvin));
245 assert_eq!(BrightnessUnit::parse(" k "), Some(BrightnessUnit::Kelvin));
246 assert_eq!(BrightnessUnit::parse("'K'"), Some(BrightnessUnit::Kelvin));
247 assert_eq!(
248 BrightnessUnit::parse("Kelvin"),
249 Some(BrightnessUnit::Kelvin)
250 );
251 assert_eq!(
252 BrightnessUnit::parse("Jy/beam"),
253 Some(BrightnessUnit::JyPerBeam)
254 );
255 assert_eq!(
256 BrightnessUnit::parse("JY BEAM-1"),
257 Some(BrightnessUnit::JyPerBeam)
258 );
259 }
260
261 #[test]
262 fn test_parse_unrecognised_is_none() {
263 assert_eq!(BrightnessUnit::parse(""), None);
265 assert_eq!(BrightnessUnit::parse("Jy/pixel"), None);
266 assert_eq!(BrightnessUnit::parse("mJy"), None);
267 }
268
269 #[test]
270 fn test_from_bunit_falls_back_to_jy_per_beam() {
271 assert_eq!(BrightnessUnit::from_bunit("K"), BrightnessUnit::Kelvin);
274 assert_eq!(
275 BrightnessUnit::from_bunit("Jy/beam"),
276 BrightnessUnit::JyPerBeam
277 );
278 assert_eq!(BrightnessUnit::from_bunit("wat"), BrightnessUnit::JyPerBeam);
279 }
280
281 #[test]
282 fn test_kelvin_skips_flux_scaling() {
283 let old = Beam::new(10.0 / 3600.0, 10.0 / 3600.0, 0.0).unwrap();
284 let new = Beam::new(20.0 / 3600.0, 20.0 / 3600.0, 0.0).unwrap();
285 let img = Array2::from_elem((32, 32), 1.0_f32);
286 let dx = 2.5 / 3600.0;
287
288 let jy = smooth(&img, &old, &new, dx, dx, None, BrightnessUnit::JyPerBeam).unwrap();
289 let k = smooth(&img, &old, &new, dx, dx, None, BrightnessUnit::Kelvin).unwrap();
290
291 let center = (16, 16);
293 assert!(jy[center] > k[center] * 1.5, "Jy/beam should be scaled up");
294 assert!(
296 (k[center] - 1.0).abs() < 1e-3,
297 "Kelvin center = {}",
298 k[center]
299 );
300 }
301}