Skip to main content

mwa_hyperdrive/cli/common/beam/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5#[cfg(test)]
6mod tests;
7
8use std::{path::PathBuf, str::FromStr};
9
10use clap::Parser;
11use log::{debug, trace};
12use ndarray::prelude::*;
13use serde::{Deserialize, Serialize};
14
15use super::{InfoPrinter, Warn};
16use crate::{
17    beam::{Beam, BeamError, BeamType, Delays, FEEBeam, NoBeam, BEAM_TYPES_COMMA_SEPARATED},
18    io::read::VisInputType,
19};
20
21lazy_static::lazy_static! {
22    static ref BEAM_TYPE_HELP: String =
23        format!("The beam model to use. Supported models: {}. Default: {}", *BEAM_TYPES_COMMA_SEPARATED, BeamType::default());
24
25    static ref NO_BEAM_HELP: String =
26        format!("Don't apply a beam response when generating a sky model. The default is to use the {} beam.", BeamType::default());
27
28    static ref BEAM_FILE_HELP: String =
29        format!("The path to the HDF5 MWA FEE beam file. Only useful if the beam type is 'fee'. If not specified, this must be provided by the MWA_BEAM_FILE environment variable.");
30}
31
32#[derive(Parser, Debug, Clone, Default, Serialize, Deserialize)]
33pub(crate) struct BeamArgs {
34    #[clap(short, long, help_heading = "BEAM", help = BEAM_TYPE_HELP.as_str())]
35    pub(crate) beam_type: Option<String>,
36
37    #[clap(long, conflicts_with("beam-type"), help_heading = "BEAM", help = NO_BEAM_HELP.as_str())]
38    #[serde(default)]
39    pub(crate) no_beam: bool,
40
41    /// If specified, use these dipole delays for the MWA pointing. e.g. 0 1 2 3
42    /// 0 1 2 3 0 1 2 3 0 1 2 3
43    #[clap(long, multiple_values(true), help_heading = "BEAM")]
44    pub(crate) delays: Option<Vec<u32>>,
45
46    /// Pretend that all MWA dipoles are alive and well, ignoring whatever is in
47    /// the metafits file.
48    #[clap(long, help_heading = "BEAM")]
49    #[serde(default)]
50    pub(crate) unity_dipole_gains: bool,
51
52    /// The path to the HDF5 MWA FEE beam file. If not specified, this must be
53    /// provided by the MWA_BEAM_FILE environment variable.
54    #[clap(long, help_heading = "BEAM", help = BEAM_FILE_HELP.as_str())]
55    pub(crate) beam_file: Option<PathBuf>,
56}
57
58impl BeamArgs {
59    pub(crate) fn merge(self, other: Self) -> Self {
60        Self {
61            beam_type: self.beam_type.or(other.beam_type),
62            no_beam: self.no_beam || other.no_beam,
63            delays: self.delays.or(other.delays),
64            unity_dipole_gains: self.unity_dipole_gains || other.unity_dipole_gains,
65            beam_file: self.beam_file.or(other.beam_file),
66        }
67    }
68
69    pub(crate) fn parse(
70        self,
71        total_num_tiles: usize,
72        data_dipole_delays: Option<Delays>,
73        dipole_gains: Option<Array2<f64>>,
74        input_data_type: Option<VisInputType>,
75    ) -> Result<Box<dyn Beam>, BeamError> {
76        let Self {
77            beam_type,
78            no_beam,
79            delays: user_dipole_delays,
80            unity_dipole_gains,
81            beam_file,
82        } = self;
83
84        let mut printer = InfoPrinter::new("Beam info".into());
85        debug!("Beam file: {beam_file:?}");
86
87        let beam_type = match (
88            no_beam,
89            beam_type.as_deref(),
90            beam_type
91                .as_deref()
92                .and_then(|b| BeamType::from_str(b).ok()),
93        ) {
94            (true, _, _) => BeamType::None,
95            (false, None, _) => BeamType::default(),
96            (false, Some(_), Some(b)) => b,
97            (false, Some(s), None) => return Err(BeamError::Unrecognised(s.to_string())),
98        };
99
100        let beam: Box<dyn Beam> = match beam_type {
101            BeamType::None => {
102                debug!("Setting up a \"NoBeam\" object");
103                printer.push_line("Not using any beam responses".into());
104                Box::new(NoBeam {
105                    num_tiles: total_num_tiles,
106                })
107            }
108
109            BeamType::FEE => {
110                debug!("Setting up a FEE beam object");
111                printer.push_line("Type: FEE".into());
112
113                let mut dipole_delays = match user_dipole_delays {
114                    Some(d) => Some(Delays::parse(d)?),
115                    None => data_dipole_delays,
116                }
117                .ok_or(BeamError::NoDelays("FEE"))?;
118                trace!("Attempting to use delays:");
119                match &dipole_delays {
120                    Delays::Full(d) => {
121                        let mut last_row = None;
122                        for (i, row) in d.outer_iter().enumerate() {
123                            if let Some(last_row) = last_row {
124                                if row == last_row {
125                                    continue;
126                                }
127                            }
128                            trace!("{i:03} {row}");
129                            last_row = Some(row);
130                        }
131                    }
132                    Delays::Partial(d) => trace!("{d:?}"),
133                }
134
135                // Check that the delays are sensible.
136                match &dipole_delays {
137                    Delays::Partial(v) => {
138                        if v.len() != 16 || v.iter().any(|&v| v > 32) {
139                            return Err(BeamError::BadDelays);
140                        }
141                    }
142
143                    Delays::Full(a) => {
144                        if a.len_of(Axis(1)) != 16 || a.iter().any(|&v| v > 32) {
145                            return Err(BeamError::BadDelays);
146                        }
147                        if a.len_of(Axis(0)) != total_num_tiles {
148                            return Err(BeamError::InconsistentDelays {
149                                num_rows: a.len_of(Axis(0)),
150                                num_tiles: total_num_tiles,
151                            });
152                        }
153                    }
154                }
155
156                let dipole_gains = if unity_dipole_gains {
157                    printer.push_line("Assuming all dipoles are \"alive\"".into());
158                    None
159                } else {
160                    // If we don't have dipole gains from the input data, then
161                    // we issue a warning that we must assume no dead dipoles.
162                    if dipole_gains.is_none() {
163                        match input_data_type {
164                            Some(VisInputType::MeasurementSet) => [
165                                "Measurement sets cannot supply dead dipole information.".into(),
166                                "Without a metafits file, we must assume all dipoles are alive.".into(),
167                                "This will make beam Jones matrices inaccurate in sky-model generation."
168                                    .into(),
169                            ]
170                            .warn(),
171                            Some(VisInputType::Uvfits) => [
172                                "uvfits files cannot supply dead dipole information.".into(),
173                                "Without a metafits file, we must assume all dipoles are alive.".into(),
174                                "This will make beam Jones matrices inaccurate in sky-model generation."
175                                    .into(),
176                            ]
177                            .warn(),
178                            Some(VisInputType::Raw) => {
179                                unreachable!("Raw data inputs always specify dipole gains")
180                            }
181                            None => (),
182                        }
183                    }
184                    dipole_gains
185                };
186                if let Some(dipole_gains) = dipole_gains.as_ref() {
187                    trace!("Attempting to use dipole gains:");
188                    let mut last_row = None;
189                    for (i, row) in dipole_gains.outer_iter().enumerate() {
190                        if let Some(last_row) = last_row {
191                            if row == last_row {
192                                continue;
193                            }
194                        }
195                        trace!("{i:03} {row}");
196                        last_row = Some(row);
197                    }
198
199                    // Currently, the only way to have dipole gains other than
200                    // zero or one is by using Aman's "DipAmps" metafits column.
201                    if dipole_gains.iter().any(|&g| g != 0.0 && g != 1.0) {
202                        printer.push_line(
203                            "Using Aman's 'DipAmps' dipole gains from the metafits".into(),
204                        );
205                    } else {
206                        let num_tiles_with_dead_dipoles = dipole_gains
207                            .outer_iter()
208                            .filter(|tile_dipole_gains| {
209                                tile_dipole_gains.iter().any(|g| g.abs() < f64::EPSILON)
210                            })
211                            .count();
212                        printer.push_line(
213                            format!(
214                                "Using dead dipole information ({num_tiles_with_dead_dipoles} tiles affected)"
215                            )
216                            .into(),
217                        );
218                    }
219                } else {
220                    // If we don't have dipole gains, we must assume all dipoles
221                    // are "alive". But, if any dipole delays are 32, then the
222                    // beam code will still ignore those dipoles. So use ideal
223                    // dipole delays for all tiles.
224                    dipole_delays.set_to_ideal_delays();
225                    let ideal_delays = dipole_delays.get_ideal_delays();
226
227                    // Warn the user if they wanted unity dipole gains but the
228                    // ideal dipole delays contain 32.
229                    if unity_dipole_gains && ideal_delays.contains(&32) {
230                        "Some ideal dipole delays are 32; these dipoles will not have unity gains"
231                            .warn()
232                    }
233                }
234
235                let beam = if let Some(bf) = beam_file {
236                    // Set up the FEE beam struct from the specified beam file.
237                    FEEBeam::new(&bf, total_num_tiles, dipole_delays, dipole_gains)?
238                } else {
239                    // Set up the FEE beam struct from the MWA_BEAM_FILE environment
240                    // variable.
241                    FEEBeam::new_from_env(total_num_tiles, dipole_delays, dipole_gains)?
242                };
243                Box::new(beam)
244            }
245        };
246
247        if let Some(d) = beam.get_ideal_dipole_delays() {
248            printer.push_block(vec![
249                format!(
250                    "Ideal dipole delays: [{:>2} {:>2} {:>2} {:>2}",
251                    d[0], d[1], d[2], d[3]
252                )
253                .into(),
254                format!(
255                    "                      {:>2} {:>2} {:>2} {:>2}",
256                    d[4], d[5], d[6], d[7]
257                )
258                .into(),
259                format!(
260                    "                      {:>2} {:>2} {:>2} {:>2}",
261                    d[8], d[9], d[10], d[11]
262                )
263                .into(),
264                format!(
265                    "                      {:>2} {:>2} {:>2} {:>2}]",
266                    d[12], d[13], d[14], d[15]
267                )
268                .into(),
269            ]);
270        }
271
272        if let Some(f) = beam.get_beam_file() {
273            printer.push_line(format!("File: {}", f.display()).into());
274        }
275
276        printer.display();
277        Ok(beam)
278    }
279}