Repository: MWATelescope/mwalib (language: python)

Issue:
misleading field `corr_raw_scale_factor` for legacy correlator observations
I need to know how much the raw values in the fits files have been scaled by for van vleck corrections.

I had assumed that [corr_raw_scale_factor](https://docs.rs/mwalib/latest/mwalib/struct.MetafitsContext.html#structfield.corr_raw_scale_factor) would give me what I need, as it is referred to in the documentation as 

> Correlator visibility scaling factor used to get the visibilities in Jansky-like units

However, this is misleading. I'll use an example that's included in the Birli repo.

```bash
 fitsheader tests/data/1254670392_avg/1254670392_20191009153257_gpubox01_00.fits | grep -i scale
BSCALE  =                0.125                                                  
BSCALE  =                0.125      
```

the scale factor here is clearly 0.125, but mwalib says it's 1.0

this is because mwalib only looks for RAWSCALE in the metafits, and defaults to 1.0 if it's not found

```rust
        // This is a new metafits key as of Oct 2021. So assume this value is 1.0 unless it is provided
        let corr_raw_scale_factor: f32 =
            get_optional_fits_key!(&mut metafits_fptr, &metafits_hdu, "RAWSCALE")?.unwrap_or(1.0);
```

I need mwalib support for reading the BSCALE values out of the raw files. I believe it's also referred to as SCALEFAC.

[This](https://github.com/RadioAstronomySoftwareGroup/pyuvdata/blob/05ee100af2e4e11c9d291c9eafc937578ef01763/src/pyuvdata/uvdata/mwa_corr_fits.py#L1459) is how pyuvdata does it


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 562635a..6f4f0fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
 
 Changes in each release are listed below.
 
+## 1.8.0 11-Nov-2024
+
+* mwalib now will detect and raise an error (`MwalibError::Fits.CfitsioIsNotReentrant`) if the CFITSIO library that mwalib is linked with has been built without the `-D_REENTRANT` directive (github issue #82).
+* Expose the FITS BSCALE in image HDUs as a single value `bscale` in `CorrelatorContext` (github issue #85).
+  * For most Legacy MWA observations, this may be a value other than 1.0. For MWAX correlator this will always be 1.0.
+  * This is mainly of interest to EoR researchers who are trying to implement Van Vleck corrections.
+* Python type stubs:
+  * Fixed pyo3 decorators to allow stub_gen to work properly / generate python stubs correctly.
+  * Used prerelease version of pyo3_stub_gen to ensure Chrono::FixedTimeOffset can have a stub generated in `MetafitsContext`.
+
 ## 1.7.2 8-Nov-2024
 
 * Update to fitsio 0.21.6 - provides FitsFile.file_path() method.
@@ -14,7 +24,7 @@ Changes in each release are listed below.
 ## 1.7.0 23-Oct-2024 (Yanked- MWAFitsFile is no longer to be used)
 
 * Bumped MSRV to 1.65.
-* Update fitsio to 0.21 and fitsio-sys to 0.5.  To make v0.21 work, a new struct, MWAFitsFile is used inplace of FitsFile, as FitsFile no longer carries the `filename` property which is needed by mwalib.
+* Update fitsio to 0.21 and fitsio-sys to 0.5. To make v0.21 work, a new struct, MWAFitsFile is used inplace of FitsFile, as FitsFile no longer carries the `filename` property which is needed by mwalib.
 * Removed Rust Report Card from README status badges. Looks like this service is abandonded.
 * Added Python .pyi stub generation to provide mwalib Python users with type and docstring information. The mwalib.pyi should get baked into the python wheels released to github and Pypi. See `bin/README.md` for caveats and more details.
 * Added CI to test compilation against cfitsio 3.x and 4.x when not using the `cfitsio-static` feature.
diff --git a/Cargo.toml b/Cargo.toml
index d8c4fe1..78243c2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mwalib"
-version = "1.7.2"
+version = "1.8.0"
 homepage = "https://github.com/MWATelescope/mwalib"
 repository = "https://github.com/MWATelescope/mwalib"
 readme = "README.md"
@@ -54,7 +54,9 @@ env_logger = { version = "~0.10", optional = true }
 ndarray = { version = "~0.16", optional = true }
 numpy = { version = "~0.22", optional = true }
 pyo3 = { version = "~0.22", features = ["chrono", "extension-module", "macros"], optional = true }
-pyo3-stub-gen = { version = "~0.6", optional = true }
+# pyo3-stub-gen = { version = "~0.6", optional = true }
+# The below pyo3-stub-gen branch has not been merged with main as of 8-Nov-2024. once it has been it should be fine to go back to using the main branch again.
+pyo3-stub-gen = { branch = "chrono_feature", optional = true, git = "https://github.com/ThatOneShortGuy/pyo3-stub-gen" }
 pyo3-stub-gen-derive = { version = "~0.6", optional = true }
 
 # "examples" feature.
diff --git a/bin/stub_gen.rs b/bin/stub_gen.rs
index a3822a9..82d0948 100644
--- a/bin/stub_gen.rs
+++ b/bin/stub_gen.rs
@@ -39,23 +39,6 @@ fn fix_stubs() -> anyhow::Result<()> {
     // After the stub is generated, we have some "manual" fixes to do
     let stubfile = String::from("mwalib.pyi");
 
-    // Import datetime
-    insert_stub_below(&stubfile, "import typing\n", "import datetime\n")?;
-
-    // Add sched_start_utc (Chrono::DateTime<FixedOffset> is not supported yet)
-    insert_stub_below(
-        &stubfile,
-        "    sched_end_unix_time_ms: int\n",
-        "    sched_start_utc: datetime.datetime\n",
-    )?;
-
-    // Add sched_end_utc (Chrono::DateTime<FixedOffset> is not supported yet)
-    insert_stub_below(
-        &stubfile,
-        "    sched_start_utc: datetime.datetime\n",
-        "    sched_end_utc: datetime.datetime\n",
-    )?;
-
     // Replace the constructors as pyo3_stub_gen seems to ignore the text_signature
     replace_stub(&stubfile,"def __new__(cls,metafits_filename,mwa_version = ...): ...","def __new__(cls, metafits_filename: str, mwa_version: typing.Optional[MWAVersion]=None)->MetafitsContext:\n        ...\n",)?;
 
@@ -87,6 +70,8 @@ fn fix_stubs() -> anyhow::Result<()> {
 /// Inserts new items in the stubfile for when pyo3 cant do it
 /// properly.
 ///
+/// Currently this function is not used, but kept in case it is needed later!
+///
 /// This will:
 /// * Open the created stubfile
 /// * Find the line in `string_to_find` (fails if not found)
@@ -106,6 +91,7 @@ fn fix_stubs() -> anyhow::Result<()> {
 /// * Result Ok if stub file was modified successfully.
 ///
 ///
+#[allow(dead_code)]
 #[cfg(feature = "python")]
 fn insert_stub_below<P: AsRef<Path>>(
     stubfile: P,
diff --git a/examples/build_c_examples.sh b/examples/build_c_examples.sh
index ceccdfc..6361749 100755
--- a/examples/build_c_examples.sh
+++ b/examples/build_c_examples.sh
@@ -2,7 +2,7 @@
 
 set -eux
 
-cargo build --release
+cargo build --release --features=cfitsio-static
 
 # Compile the example C code
 gcc -O3 \
diff --git a/mwalib.pyi b/mwalib.pyi
index 114868c..c48801b 100644
--- a/mwalib.pyi
+++ b/mwalib.pyi
@@ -332,6 +332,7 @@ class CorrelatorContext:
     num_gpubox_files: int
     gpubox_batches: list[GpuBoxBatch]
     gpubox_time_map: dict[int, dict[int, tuple[int, int]]]
+    bscale: float
     def __new__(cls, metafits_filename: str, gpubox_filenames: list[str]) -> CorrelatorContext: ...
     def get_fine_chan_freqs_hz_array(self, corr_coarse_chan_indices: typing.Sequence[int]) -> list[float]:
         r"""
diff --git a/src/antenna/mod.rs b/src/antenna/mod.rs
index 29f0eed..c771334 100644
--- a/src/antenna/mod.rs
+++ b/src/antenna/mod.rs
@@ -7,6 +7,8 @@
 use crate::rfinput::*;
 use std::fmt;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
@@ -14,8 +16,7 @@ use pyo3_stub_gen_derive::gen_stub_pyclass;
 mod test;
 
 /// Structure for storing MWA antennas (tiles without polarisation) information from the metafits file
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct Antenna {
     /// This is the antenna number.
diff --git a/src/baseline/mod.rs b/src/baseline/mod.rs
index 714043f..f716adf 100644
--- a/src/baseline/mod.rs
+++ b/src/baseline/mod.rs
@@ -7,14 +7,16 @@
 use crate::misc;
 use std::fmt;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
 #[cfg(test)]
 mod test;
+
 /// This is a struct for our baselines, so callers know the antenna ordering
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct Baseline {
     /// Index in the mwalibContext.antenna array for antenna1 for this baseline    
diff --git a/src/coarse_channel/mod.rs b/src/coarse_channel/mod.rs
index 3f1a5d2..28e5415 100644
--- a/src/coarse_channel/mod.rs
+++ b/src/coarse_channel/mod.rs
@@ -12,6 +12,8 @@ use error::CoarseChannelError;
 use fitsio::{hdu::FitsHdu, FitsFile};
 use std::fmt;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
@@ -19,8 +21,7 @@ use pyo3_stub_gen_derive::gen_stub_pyclass;
 mod test;
 
 /// This is a struct for coarse channels
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct CoarseChannel {
     /// Correlator channel is 0 indexed (0..N-1)
diff --git a/src/convert/mod.rs b/src/convert/mod.rs
index 7ea5c2c..15f4a8c 100644
--- a/src/convert/mod.rs
+++ b/src/convert/mod.rs
@@ -12,6 +12,11 @@ use crate::rfinput::*;
 use log::trace;
 use std::fmt;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
+#[cfg(feature = "python")]
+use pyo3_stub_gen_derive::gen_stub_pyclass;
+
 #[cfg(test)]
 mod test;
 
@@ -35,8 +40,8 @@ fn fine_pfb_reorder(input: usize) -> usize {
 }
 
 /// Structure for storing where in the input visibilities to get the specified baseline when converting
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
 pub(crate) struct LegacyConversionBaseline {
     pub baseline: usize,    // baseline index
     pub ant1: usize,        // antenna1 index
diff --git a/src/correlator_context/mod.rs b/src/correlator_context/mod.rs
index ff25885..eb12630 100644
--- a/src/correlator_context/mod.rs
+++ b/src/correlator_context/mod.rs
@@ -5,6 +5,7 @@
 //! The main interface to MWA data.
 
 use fitsio::FitsFile;
+use log::warn;
 use std::collections::BTreeMap;
 use std::fmt;
 use std::path::Path;
@@ -17,9 +18,10 @@ use crate::metafits_context::*;
 use crate::timestep::*;
 use crate::*;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
-
 #[cfg(feature = "python")]
 mod python;
 
@@ -29,8 +31,7 @@ mod test;
 ///
 /// This represents the basic metadata and methods for an MWA correlator observation.
 ///
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Debug)]
 pub struct CorrelatorContext {
     /// Observation Metadata obtained from the metafits file
@@ -124,6 +125,8 @@ pub struct CorrelatorContext {
     pub gpubox_time_map: BTreeMap<u64, BTreeMap<usize, (usize, usize)>>,
     /// A conversion table to optimise reading of legacy MWA HDUs
     pub(crate) legacy_conversion_table: Vec<LegacyConversionBaseline>,
+    /// BSCALE- FITS BSCALE or SCALEFAC value set on the visibility HDUs (used in Legacy Correlator only)
+    pub bscale: f32,
 }
 
 impl CorrelatorContext {
@@ -147,6 +150,11 @@ impl CorrelatorContext {
         metafits_filename: P,
         gpubox_filenames: &[P2],
     ) -> Result<Self, MwalibError> {
+        // Check CFITSIO is reentrant before proceeding
+        if !fits_read::is_fitsio_reentrant() {
+            return Err(MwalibError::Fits(FitsError::CfitsioIsNotReentrant));
+        }
+
         Self::new_inner(metafits_filename.as_ref(), gpubox_filenames)
     }
 
@@ -167,6 +175,15 @@ impl CorrelatorContext {
         // Update the metafits now that we know the mwa_version
         metafits_context.mwa_version = Some(gpubox_info.mwa_version);
 
+        // Determine BSCALE from 1st visibility HDU of each gpubox file
+        let bscale: f32 = match metafits_context.mwa_version {
+            Some(MWAVersion::CorrOldLegacy) | Some(MWAVersion::CorrLegacy) => {
+                Self::get_bscale_from_gpubox_files(metafits_context.obs_id, gpubox_filenames)
+            }
+            // Only Legacy MWA Correlator observations have BSCALE set to anything other than 1.0
+            _ => 1.0,
+        };
+
         // Populate metafits coarse channels and timesteps now that we know what MWA Version we are dealing with
         // Populate the coarse channels
         metafits_context.populate_expected_coarse_channels(gpubox_info.mwa_version)?;
@@ -390,6 +407,7 @@ impl CorrelatorContext {
             num_timestep_coarse_chan_weight_floats: weight_floats,
             num_gpubox_files: gpubox_filenames.len(),
             legacy_conversion_table,
+            bscale,
         })
     }
 
@@ -902,6 +920,63 @@ impl CorrelatorContext {
 
         Ok(())
     }
+
+    /// Returns the BSCALE/SCALEFAC value used in the visibility FITS HDUs. The value
+    /// is only non-one in Legacy Correlator observations and is most of interest
+    /// only to EoR researchers.
+    ///    
+    /// # Arguments
+    ///
+    /// * `obs_id` - The observation ID of this observation
+    ///
+    /// * `gpubox_filenames` - A reference to a vector of paths to the gpubox files
+    ///
+    /// # Returns
+    ///
+    /// * an f32 which is either the BSCALE/SCALEFAC present in the visibility gpuboxfiles or 1.0
+    ///
+    fn get_bscale_from_gpubox_files<P: AsRef<Path>>(obs_id: u32, gpubox_filenames: &[P]) -> f32 {
+        let mut scale_factor: Option<f32> = None;
+
+        for raw_path in gpubox_filenames {
+            // Open each gpubox file
+            let mut fptr = fitsio::FitsFile::open(raw_path).unwrap();
+            // Only check the first visibilities HDU in each gpubox file
+            let hdu1 = fits_open_hdu!(&mut fptr, 1).unwrap();
+            // Check for SCALEFAC and BSCALE
+            for key in ["SCALEFAC", "BSCALE"] {
+                let this_scale_factor: Option<f32> =
+                    get_optional_fits_key!(&mut fptr, &hdu1, key).unwrap();
+                match (scale_factor, this_scale_factor) {
+                    (Some(sf), Some(this_sf)) => {
+                        assert!(
+                            ((sf - this_sf).abs() < f32::EPSILON),
+                            "Different scale factors found in gpubox files: {} and {}",
+                            sf,
+                            this_sf
+                        );
+                    }
+                    (None, Some(this_sf)) => {
+                        scale_factor = Some(this_sf);
+                    }
+                    _ => {}
+                }
+            }
+        }
+        // If BSCALE/SCALEFAC was found then return it, otherwise
+        // Check if the obsid is < 1096160568 then return 0.25 otherwise return 1.0
+        // TODO we should really check this "0.25" assumption!
+        match (scale_factor, obs_id) {
+            (Some(sf), _) => sf,
+            // according to pyuvdata "correlator did a divide by 4 before october 2014"
+            // https://github.com/RadioAstronomySoftwareGroup/pyuvdata/blob/05ee100af2e4e11c9d291c9eafc937578ef01763/src/pyuvdata/uvdata/mwa_corr_fits.py#L1464
+            (None, t) if t < 1096160568 => 0.25,
+            _ => {
+                warn!("No scale factor found in metafits or gpufits files, defaulting to 1.0");
+                1.0
+            }
+        }
+    }
 }
 
 /// Implements fmt::Display for CorrelatorContext struct
@@ -958,6 +1033,8 @@ impl fmt::Display for CorrelatorContext {
             Memory usage per scan:      {scan_size} MiB,
 
             gpubox batches:             {batches:#?},
+
+            BSCALE/SCALEFAC:            {bscale},
         )"#,
             metafits_context = self.metafits_context,
             corr_ver = self.mwa_version,
@@ -992,6 +1069,7 @@ impl fmt::Display for CorrelatorContext {
             hdu_size = size,
             scan_size = size * self.num_gpubox_files as f64,
             batches = self.gpubox_batches,
+            bscale = self.bscale
         )
     }
 }
diff --git a/src/correlator_context/python.rs b/src/correlator_context/python.rs
index 980663e..1f1b888 100644
--- a/src/correlator_context/python.rs
+++ b/src/correlator_context/python.rs
@@ -15,8 +15,6 @@ use numpy::PyArray2;
 #[cfg(feature = "python")]
 use numpy::PyArray3;
 #[cfg(feature = "python")]
-use pyo3::prelude::*;
-#[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pymethods;
 
 #[cfg_attr(feature = "python", gen_stub_pymethods)]
diff --git a/src/error.rs b/src/error.rs
index ee67478..2d20dd8 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -9,6 +9,10 @@ use thiserror::Error;
 /// MwalibError subtypes
 #[derive(Error, Debug)]
 pub enum MwalibError {
+    /// An error derived from `CfitsioError`.
+    //#[error("{0}")]
+    //Cfitsio(#[from] crate::fits_read::error::CfitsioError),
+
     /// An error derived from `FitsError`.
     #[error("{0}")]
     Fits(#[from] crate::fits_read::error::FitsError),
diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs
index 0d216fc..174d99f 100644
--- a/src/ffi/mod.rs
+++ b/src/ffi/mod.rs
@@ -2296,6 +2296,8 @@ pub struct CorrelatorMetadata {
     pub num_timestep_coarse_chan_weight_floats: usize,
     /// This is the number of gpubox files *per batch*.
     pub num_gpubox_files: usize,
+    /// BSCALE- FITS BSCALE or SCALEFAC value set on the visibility HDUs (used in Legacy Correlator only)
+    pub bscale: f32,
 }
 
 /// This returns a struct containing the `CorrelatorContext` metadata
@@ -2426,6 +2428,7 @@ pub unsafe extern "C" fn mwalib_correlator_metadata_get(
             gpubox_batches: _, // This is currently not provided to FFI as it is private
             gpubox_time_map: _, // This is currently not provided to FFI
             legacy_conversion_table: _, // This is currently not provided to FFI as it is private
+            bscale,
         } = context;
         CorrelatorMetadata {
             mwa_version: *mwa_version,
@@ -2471,6 +2474,7 @@ pub unsafe extern "C" fn mwalib_correlator_metadata_get(
             num_timestep_coarse_chan_floats: *num_timestep_coarse_chan_floats,
             num_timestep_coarse_chan_weight_floats: *num_timestep_coarse_chan_weight_floats,
             num_gpubox_files: *num_gpubox_files,
+            bscale: *bscale,
         }
     };
 
diff --git a/src/fits_read/error.rs b/src/fits_read/error.rs
index 49889a9..50f64bc 100644
--- a/src/fits_read/error.rs
+++ b/src/fits_read/error.rs
@@ -11,6 +11,10 @@ use thiserror::Error;
 /// FitsError subtypes - mainly used by CorrelatorContext
 #[derive(Error, Debug)]
 pub enum FitsError {
+    /// CFITSIO was not compiled with the REENTRANT directive
+    #[error("mwalib has been compiled with a CFITSIO library which was not built with the -DREENTRANT directive")]
+    CfitsioIsNotReentrant,
+
     /// Error when opening a fits file.
     #[error("{source_file}:{source_line}\nCouldn't open {fits_filename}: {fits_error}")]
     Open {
diff --git a/src/fits_read/mod.rs b/src/fits_read/mod.rs
index 627af16..3fcf586 100644
--- a/src/fits_read/mod.rs
+++ b/src/fits_read/mod.rs
@@ -975,3 +975,13 @@ pub fn read_cell_array_f64(
         }
     }
 }
+
+/// Returns True if CFITSIO was compiled with the REENTRANT directive
+///
+/// # Returns
+///
+/// * boolean value indicating if CFITSIO is compiled with the REENTRANT directive.
+///
+pub fn is_fitsio_reentrant() -> bool {
+    unsafe { fitsio_sys::fits_is_reentrant() == 1 }
+}
diff --git a/src/gpubox_files/mod.rs b/src/gpubox_files/mod.rs
index 85eb786..13731ae 100644
--- a/src/gpubox_files/mod.rs
+++ b/src/gpubox_files/mod.rs
@@ -20,6 +20,8 @@ pub use error::GpuboxError;
 
 use fitsio::FitsFile;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
@@ -37,8 +39,7 @@ pub(crate) struct ObsTimesAndChans {
 
 /// This represents one group of gpubox files with the same "batch" identitifer.
 /// e.g. obsid_datetime_chan_batch
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct GpuBoxBatch {
     /// Batch number: 00,01,02..n.
@@ -68,8 +69,7 @@ impl fmt::Debug for GpuBoxBatch {
 }
 
 /// This represents one gpubox file
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct GpuBoxFile {
     /// Filename of gpubox file
diff --git a/src/metafits_context/mod.rs b/src/metafits_context/mod.rs
index 6d9b1f3..b3d7c3f 100644
--- a/src/metafits_context/mod.rs
+++ b/src/metafits_context/mod.rs
@@ -26,6 +26,8 @@ pub mod error;
 #[cfg(test)]
 mod test;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 #[cfg(feature = "python")]
@@ -345,8 +347,7 @@ impl std::str::FromStr for MWAMode {
 ///
 /// Metafits context. This represents the basic metadata for an MWA observation.
 ///
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone, Debug)]
 pub struct MetafitsContext {
     /// mwa version
@@ -538,6 +539,11 @@ impl MetafitsContext {
         metafits: P,
         mwa_version: Option<MWAVersion>,
     ) -> Result<Self, MwalibError> {
+        // Check CFITSIO is reentrant before proceeding
+        if !fits_read::is_fitsio_reentrant() {
+            return Err(MwalibError::Fits(FitsError::CfitsioIsNotReentrant));
+        }
+
         Self::new_inner(metafits.as_ref(), mwa_version)
     }
 
diff --git a/src/metafits_context/python.rs b/src/metafits_context/python.rs
index 2305202..e0e4a6a 100644
--- a/src/metafits_context/python.rs
+++ b/src/metafits_context/python.rs
@@ -6,8 +6,6 @@
 #[cfg(feature = "python")]
 use super::*;
 #[cfg(feature = "python")]
-use pyo3::prelude::*;
-#[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pymethods;
 
 #[cfg_attr(feature = "python", gen_stub_pymethods)]
diff --git a/src/rfinput/mod.rs b/src/rfinput/mod.rs
index e14de2b..ca93b3f 100644
--- a/src/rfinput/mod.rs
+++ b/src/rfinput/mod.rs
@@ -14,9 +14,9 @@ use fitsio::FitsFile;
 use std::fmt;
 
 #[cfg(feature = "python")]
-use pyo3_stub_gen_derive::gen_stub_pyclass;
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
-use pyo3_stub_gen_derive::gen_stub_pyclass_enum;
+use pyo3_stub_gen_derive::{gen_stub_pyclass, gen_stub_pyclass_enum};
 
 #[cfg(test)]
 mod test;
@@ -256,8 +256,7 @@ struct RfInputMetafitsCalibDataTableRow {
 }
 
 /// Structure for storing MWA rf_chains (tile with polarisation) information from the metafits file
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct Rfinput {
     /// This is the metafits order (0-n inputs)
diff --git a/src/signal_chain_correction/mod.rs b/src/signal_chain_correction/mod.rs
index 50eb8cd..85907b9 100644
--- a/src/signal_chain_correction/mod.rs
+++ b/src/signal_chain_correction/mod.rs
@@ -2,14 +2,15 @@ use crate::rfinput::ReceiverType;
 use crate::MAX_RECEIVER_CHANNELS;
 use std::fmt;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
 ///
 /// Signal chain correction table
 ///
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone, Debug, PartialEq)]
 #[repr(C)]
 pub struct SignalChainCorrection {
diff --git a/src/timestep/mod.rs b/src/timestep/mod.rs
index 0e562f8..341dd7a 100644
--- a/src/timestep/mod.rs
+++ b/src/timestep/mod.rs
@@ -14,13 +14,14 @@ use std::fmt;
 #[cfg(test)]
 mod test;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
 /// This is a struct for our timesteps
 /// NOTE: correlator timesteps use unix time, voltage timesteps use gpstime, but we convert the two depending on what we are given
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct TimeStep {
     /// UNIX time (in milliseconds to avoid floating point inaccuracy)
diff --git a/src/voltage_context/mod.rs b/src/voltage_context/mod.rs
index e3379d8..d9f0f95 100644
--- a/src/voltage_context/mod.rs
+++ b/src/voltage_context/mod.rs
@@ -15,6 +15,8 @@ use std::fs::File;
 use std::io::{Read, Seek, SeekFrom};
 use std::path::Path;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
@@ -27,8 +29,7 @@ pub(crate) mod test; // It's pub crate because I reuse some test code in the ffi
 ///
 /// This represents the basic metadata and methods for an MWA voltage capture system (VCS) observation.
 ///
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Debug)]
 pub struct VoltageContext {
     /// Observation Metadata obtained from the metafits file
@@ -165,6 +166,11 @@ impl VoltageContext {
         metafits_filename: P,
         voltage_filenames: &[P2],
     ) -> Result<Self, MwalibError> {
+        // Check CFITSIO is reentrant before proceeding
+        if !fits_read::is_fitsio_reentrant() {
+            return Err(MwalibError::Fits(FitsError::CfitsioIsNotReentrant));
+        }
+
         Self::new_inner(metafits_filename.as_ref(), voltage_filenames)
     }
 
diff --git a/src/voltage_context/python.rs b/src/voltage_context/python.rs
index ebebf96..3891627 100644
--- a/src/voltage_context/python.rs
+++ b/src/voltage_context/python.rs
@@ -12,8 +12,6 @@ use ndarray::Dim;
 #[cfg(feature = "python")]
 use numpy::PyArray;
 #[cfg(feature = "python")]
-use pyo3::prelude::*;
-#[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pymethods;
 
 #[cfg_attr(feature = "python", gen_stub_pymethods)]
diff --git a/src/voltage_files/mod.rs b/src/voltage_files/mod.rs
index 141e1ac..07717f8 100644
--- a/src/voltage_files/mod.rs
+++ b/src/voltage_files/mod.rs
@@ -14,6 +14,8 @@ use std::collections::HashSet;
 use std::fmt;
 use std::path::Path;
 
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
 #[cfg(feature = "python")]
 use pyo3_stub_gen_derive::gen_stub_pyclass;
 
@@ -32,8 +34,7 @@ pub(crate) struct ObsTimesAndChans {
 /// e.g.
 /// MWA Legacy: obsid_gpstime_datetime_chan
 /// MWAX      : obsid_gpstime_datetime_chan
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct VoltageFileBatch {
     // GPS second of this observation. e.g. 1234567890
@@ -63,8 +64,7 @@ impl fmt::Debug for VoltageFileBatch {
 }
 
 /// This represents one voltage file
-#[cfg_attr(feature = "python", gen_stub_pyclass)]
-#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]
+#[cfg_attr(feature = "python", gen_stub_pyclass, pyclass(get_all, set_all))]
 #[derive(Clone)]
 pub struct VoltageFile {
     /// Filename of voltage file


Test patch:
diff --git a/src/correlator_context/test.rs b/src/correlator_context/test.rs
index a4c8bd5..6d063d2 100644
--- a/src/correlator_context/test.rs
+++ b/src/correlator_context/test.rs
@@ -177,6 +177,8 @@ fn test_context_legacy_v1() {
         context.metafits_context.num_metafits_fine_chan_freqs,
         context.metafits_context.metafits_fine_chan_freqs_hz.len()
     );
+
+    assert_eq!(context.bscale, 0.5);
 }
 
 #[test]
@@ -272,7 +274,9 @@ fn test_context_mwax() {
     assert_eq!(
         context.num_timestep_coarse_chan_weight_floats,
         ((128 * 129) / 2) * 4
-    )
+    );
+
+    assert_eq!(context.bscale, 1.0);
 }
 
 #[test]
diff --git a/src/ffi/test.rs b/src/ffi/test.rs
index 0e47031..755938d 100644
--- a/src/ffi/test.rs
+++ b/src/ffi/test.rs
@@ -2292,6 +2292,9 @@ fn test_mwalib_correlator_metadata_get_valid() {
         );
         assert_eq!(item[0].unix_time_ms, 1_417_468_096_000);
 
+        // Check bscale
+        assert_eq!(correlator_metadata.bscale, 0.5);
+
         // So that the next free works, we set the pointer to null (the ffi_boxed_slice_to_array effectively freed the timestep array memory - as far as C/FFI is concerned)
         correlator_metadata.timesteps = std::ptr::null_mut();
 
diff --git a/src/fits_read/test.rs b/src/fits_read/test.rs
index 8052349..29bdf97 100644
--- a/src/fits_read/test.rs
+++ b/src/fits_read/test.rs
@@ -993,3 +993,8 @@ fn test_read_cell_array_f64() {
     let asdf = read_cell_array_f64(&mut fptr, &hdu, "NotReal", 0, 24);
     assert!(asdf.is_err());
 }
+
+#[test]
+fn test_is_fitsio_reentrant() {
+    assert!(is_fitsio_reentrant())
+}
diff --git a/tests/test_correlator_context.py b/tests/test_correlator_context.py
index b798d76..612ad3b 100644
--- a/tests/test_correlator_context.py
+++ b/tests/test_correlator_context.py
@@ -25,20 +25,22 @@
 
 @pytest.fixture
 def mwax_cc() -> mwalib.CorrelatorContext:
-    return mwalib.CorrelatorContext(
-        MWAX_CORRELATOR_METAFITS, MWAX_CORRELATOR_GPUBOX_FILES
-    )
+    return mwalib.CorrelatorContext(MWAX_CORRELATOR_METAFITS, MWAX_CORRELATOR_GPUBOX_FILES)
 
 
 @pytest.fixture
-def legacy_cc() -> mwalib.CorrelatorContext:    
-    return mwalib.CorrelatorContext(
-        LEGACY_CORRELATOR_METAFITS, LEGACY_CORRELATOR_GPUBOX_FILES
-    )
+def legacy_cc() -> mwalib.CorrelatorContext:
+    return mwalib.CorrelatorContext(LEGACY_CORRELATOR_METAFITS, LEGACY_CORRELATOR_GPUBOX_FILES)
+
 
+def test_legacy_corr_context_attributes(legacy_cc: mwalib.CorrelatorContext):
+    assert legacy_cc.mwa_version == mwalib.MWAVersion.CorrLegacy
+    assert legacy_cc.bscale == 0.5
 
-def test_mwax_corr_context_mwa_version(mwax_cc: mwalib.CorrelatorContext):
+
+def test_mwax_corr_context_attributes(mwax_cc: mwalib.CorrelatorContext):
     assert mwax_cc.mwa_version == mwalib.MWAVersion.CorrMWAXv2
+    assert mwax_cc.bscale == 1.0
 
 
 def test_mwax_corr_context_read_visibilities(mwax_cc: mwalib.CorrelatorContext):
@@ -64,9 +66,7 @@ def test_mwax_corr_context_read_visibilities(mwax_cc: mwalib.CorrelatorContext):
     )
 
     # Check the sums are equial
-    assert np.sum(data_by_bl, dtype=np.float64) == np.sum(
-        data_by_freq, dtype=np.float64
-    )
+    assert np.sum(data_by_bl, dtype=np.float64) == np.sum(data_by_freq, dtype=np.float64)
 
 
 def test_mwax_corr_context_read_weights_by_baseline(mwax_cc: mwalib.CorrelatorContext):
diff --git a/tools/test_msrv.sh b/tools/test_msrv.sh
index 34aa973..9aacf7e 100755
--- a/tools/test_msrv.sh
+++ b/tools/test_msrv.sh
@@ -48,7 +48,9 @@ pytest
 
 # Build C examples
 echo "Building C examples..."
-examples/build_c_examples.sh
+pushd examples
+./build_c_examples.sh
+popd
 
 # Switch back to this dir
 popd
\ No newline at end of file