diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 31fac9b..85cc356 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -11,8 +11,7 @@ on: env: RUST_BACKTRACE: 1 CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - MWALIB_LINK_STATIC_CFITSIO: 1 + CARGO_INCREMENTAL: 0 jobs: generate_coverage: diff --git a/.github/workflows/run_cfitsio_tests.yml b/.github/workflows/run_cfitsio_tests.yml new file mode 100644 index 0000000..1d9b3da --- /dev/null +++ b/.github/workflows/run_cfitsio_tests.yml @@ -0,0 +1,111 @@ +name: cfitsio tests + +on: + push: + tags-ignore: + - '**' + branches: + - '**' + pull_request: + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + test_cfitsio_3: + name: Test cfitsio 3.49 on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + # see https://github.com/actions/runner-images?tab=readme-ov-file#available-images for runner types + os: [ubuntu-latest, macos-13, macos-14, macos-15] # macos-13 is x86_64, macos-14 & 15 are Arm64 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: (macos) install automake and autoconf + if: ${{ startsWith(matrix.os, 'macOS') }} + run: | + brew install automake autoconf + + - name: Install stable minimal toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + - name: Install cfitsio 3.49 + run: | + curl "https://heasarc.gsfc.nasa.gov/FTP/software/fitsio/c/cfitsio-3.49.tar.gz" -o cfitsio.tar.gz + tar -xf cfitsio.tar.gz + rm cfitsio.tar.gz + cd cfitsio-3.49 + # Enabling SSE2/SSSE3 could cause portability problems, but it's unlikely that anyone + # is using such a CPU... + # https://stackoverflow.com/questions/52858556/most-recent-processor-without-support-of-ssse3-instructions + # Disabling curl just means you cannot fits_open() using a URL. + CFLAGS="-O3" ./configure --prefix=/usr/local --enable-reentrant --enable-sse2 --enable-ssse3 --disable-curl + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + make -j + sudo make install + sudo ldconfig + + elif [[ "$OSTYPE" == "darwin"* ]]; then + sudo make shared + sudo make install + fi + + cd .. + + - name: Run tests run on latest stable rust + run: cargo test --features examples + + test_cfitsio_4: + name: Test cfitsio 4.5.0 on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + # see https://github.com/actions/runner-images?tab=readme-ov-file#available-images for runner types + os: [ubuntu-latest, macos-13, macos-14, macos-15] # macos-13 is x86_64, macos-14 & 15 are Arm64 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: (macos) install automake and autoconf + if: ${{ startsWith(matrix.os, 'macOS') }} + run: | + brew install automake autoconf + + - name: Install stable minimal toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + + - name: Install cfitsio 4.5.0 + run: | + curl "https://heasarc.gsfc.nasa.gov/FTP/software/fitsio/c/cfitsio-4.5.0.tar.gz" -o cfitsio.tar.gz + tar -xf cfitsio.tar.gz + rm cfitsio.tar.gz + cd cfitsio-4.5.0 + # Enabling SSE2/SSSE3 could cause portability problems, but it's unlikely that anyone + # is using such a CPU... + # https://stackoverflow.com/questions/52858556/most-recent-processor-without-support-of-ssse3-instructions + # Disabling curl just means you cannot fits_open() using a URL. + CFLAGS="-O3" ./configure --prefix=/usr/local --enable-reentrant --enable-sse2 --enable-ssse3 --disable-curl + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + make -j + sudo make install + sudo ldconfig + + elif [[ "$OSTYPE" == "darwin"* ]]; then + sudo make + sudo make install + fi + + cd .. + + - name: Run tests run on latest stable rust + run: cargo test --features examples \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index a999413..cf5daf6 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -42,4 +42,4 @@ jobs: run: | MIN_RUST=$(grep -m1 "rust-version" Cargo.toml | sed 's|.*\"\(.*\)\"|\1|') ~/.cargo/bin/rustup install $MIN_RUST --profile minimal - cargo +${MIN_RUST} test --features cfitsio-static, examples \ No newline at end of file + cargo +${MIN_RUST} test --features cfitsio-static,examples \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ced6d95..6a6623e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ Changes in each release are listed below. +## 1.7.0 23-Oct-2024 + +* Bumped MSRV to 1.65. +* Update fitsio to 0.21 and fitsio-sys to 0.5. +* 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. + ## 1.6.0 18-Oct-2024 * Updated ndarray to 0.16 diff --git a/Cargo.toml b/Cargo.toml index 459e55e..edac0bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,44 @@ [package] name = "mwalib" -version = "1.6.0" +version = "1.7.0" homepage = "https://github.com/MWATelescope/mwalib" repository = "https://github.com/MWATelescope/mwalib" readme = "README.md" authors = ["Greg Sleap ", "Christopher H. Jordan "] edition = "2021" -rust-version = "1.64" +rust-version = "1.65" description = "A library to simplify reading Murchison Widefield Array (MWA) raw visibilities, voltages and metadata." license = "MPL-2.0" keywords = ["radioastronomy", "mwa", "astronomy"] categories = ["science","parsing"] exclude = ["test_files/*", "tools/*",".github/*"] +[[bin]] +name = "stub_gen" +path = "bin/stub_gen.rs" +doc = false +required-features = ["python"] + # Make a rust library, as well as static and C-compatible dynamic libraries # available as "libmwalib.a" and "libmwalib.so". [lib] crate-type = ["rlib", "staticlib", "cdylib"] [features] +# default +default = ["cfitsio-static", "examples"] # Compile cfitsio from source and link it statically. cfitsio-static = ["fitsio-sys/fitsio-src"] # Enable optional features needed by examples. examples = ["anyhow", "clap", "env_logger"] # Enable python -python = ["pyo3", "numpy", "ndarray"] +python = ["anyhow", "env_logger", "ndarray", "numpy", "pyo3", "pyo3-stub-gen", "pyo3-stub-gen-derive"] [dependencies] chrono = "0.4.38" -fitsio = "~0.20" -fitsio-sys = "~0.4" +fitsio = "~0.21" +fitsio-sys = "~0.5" lazy_static = "~1.5" libc = "~0.2" log = "~0.4" @@ -40,15 +48,19 @@ rayon = "~1.10" regex = "~1.9" thiserror = "~1.0" -# "python" feature. -pyo3 = { version = "~0.22", features = ["chrono", "extension-module"], optional = true } -numpy = { version = "~0.22", optional = true } +# "python" and examples features +anyhow = { version = "~1.0", optional = true } +env_logger = { version = "~0.10", optional = true } + +# "python" feature 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-derive = { version = "~0.6", optional = true } # "examples" feature. -anyhow = { version = "~1.0", optional = true } clap = { version = "~4.1", features = ["derive"], optional = true } -env_logger = { version = "~0.10", optional = true } [dev-dependencies] csv = "~1.3" @@ -85,4 +97,4 @@ required-features = ["examples"] [[example]] name = "mwalib-sum-first-fine-channel-gpubox-hdus" -required-features = ["examples"] +required-features = ["examples"] \ No newline at end of file diff --git a/README.md b/README.md index 4e3734f..4a9b551 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ ![Crates.io](https://img.shields.io/crates/d/mwalib) ![Crates.io](https://img.shields.io/crates/l/mwalib) [![docs](https://docs.rs/mwalib/badge.svg)](https://docs.rs/crate/mwalib/latest) -[![Rust Report Card](https://rust-reportcard.xuri.me/badge/github.com/MWATelescope/mwalib)](https://rust-reportcard.xuri.me/report/github.com/MWATelescope/mwalib) mwalib is an MWA library to read raw visibilities, voltages and metadata into a common structure. mwalib supports the existing "legacy" MWA correlator, as well as the "MWAX" correlator observations. This library diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..54a8650 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,20 @@ +# mwalib python pyi stub generation + +## How it works + +1. Build mwalib using: `cargo build --all-features` +2. Run the stub generator: `target/debug/stub_gen` +3. At this point you can view the `mwalib.pyi` file and see what Python stubs were generated. +4. To test, build a python wheel to test with: `maturin build --all-features --out dist`. +5. In a freah Python environment install the wheel: `pip install dist/mwalib-1.7.0-cp313-cp313-manylinux_2_34_x86_64.whl`. +6. Open a python file in your IDE and hopefully you have some type info and some doc strings. + +## Caveats + +* Due to [this issue](https://github.com/Jij-Inc/pyo3-stub-gen/issues/93) and [this issue](https://github.com/PyO3/pyo3/issues/780), the codem as of mwalib 1.7.0 does NOT produce a full mwalib.pyi file, due to the fact that the stub generation requires the `python` feature and to get all the struct members to appear in the stub you need to decorate each member with `#[pyo3(get,set)]` but this decorator does not work with the `#[cfg_attr(feature = "python", pyo3(get,set))]` syntax needed to allow mwalib to be compiled without the `python` feature! So to get past this, I have removed the `#[cfg_attr(feature = "python", pyo3(get,set))]` syntax from all struct members and changed `#[cfg_attr(feature = "python", pyo3::pyclass]` on each struct to `#[cfg_attr(feature = "python", pyo3::pyclass(get_all, set_all))]` which will still create the python bindings but none of the struct members will be emitted when generating the stub file! + +* Docstrings for `#[new]` methods on structs/classes do not get generated. + +* `__enter__` method for a class gets the wrong generated stub so I have to override it (see below). + +* Some other manual fixes can be seen in `bin/stubgen.rs`. Hacky but we live in an imperfect world! diff --git a/bin/stub_gen.rs b/bin/stub_gen.rs new file mode 100644 index 0000000..a3822a9 --- /dev/null +++ b/bin/stub_gen.rs @@ -0,0 +1,254 @@ +extern crate mwalib; + +#[cfg(feature = "python")] +use std::env; +#[cfg(feature = "python")] +use std::fs::File; +#[cfg(feature = "python")] +use std::io::{Read, Write}; +#[cfg(feature = "python")] +use std::path::Path; + +#[cfg(test)] +#[cfg(feature = "python")] +use tempdir::TempDir; + +#[cfg(feature = "python")] +fn main() -> anyhow::Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init(); + + generate_stubs()?; + + fix_stubs()?; + + anyhow::Ok(()) +} + +#[cfg(feature = "python")] +fn generate_stubs() -> anyhow::Result<()> { + // Generating the stub requires the below env variable to be set for some reason? + env::set_var("CARGO_MANIFEST_DIR", env::current_dir()?); + let stub = mwalib::python::stub_info()?; + stub.generate()?; + + Ok(()) +} + +#[cfg(feature = "python")] +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 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 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",)?; + + replace_stub(&stubfile, "def __new__(cls,metafits_filename,gpubox_filenames): ...", "def __new__(cls, metafits_filename: str, gpubox_filenames: list[str])->CorrelatorContext:\n ...\n")?; + + replace_stub(&stubfile,"def __new__(cls,metafits_filename,voltage_filenames): ...","def __new__(cls, metafits_filename: str, voltage_filenames: list[str])->VoltageContext:\n ...\n",)?; + + replace_stub( + &stubfile, + "def __enter__(self, slf:MetafitsContext) -> MetafitsContext:", + "def __enter__(self) -> MetafitsContext:", + )?; + + replace_stub( + &stubfile, + "def __enter__(self, slf:CorrelatorContext) -> CorrelatorContext:", + "def __enter__(self) -> CorrelatorContext:", + )?; + + replace_stub( + &stubfile, + "def __enter__(self, slf:VoltageContext) -> VoltageContext:", + "def __enter__(self) -> VoltageContext:", + )?; + + Ok(()) +} + +/// Inserts new items in the stubfile for when pyo3 cant do it +/// properly. +/// +/// This will: +/// * Open the created stubfile +/// * Find the line in `string_to_find` (fails if not found) +/// * Add a newline and `string_to_add_below` (fails if cannot) +/// +/// # Arguments +/// +/// * `stubfile` - `Path` representing filename of the stubfile to edit +/// +/// * `string_to_find` - string to find, so we know where in the file to make the insert +/// +/// * `string_to_add_below` - string to add on a new line below `string_to_find` +/// +/// +/// # Returns +/// +/// * Result Ok if stub file was modified successfully. +/// +/// +#[cfg(feature = "python")] +fn insert_stub_below>( + stubfile: P, + string_to_find: &str, + string_to_add_below: &str, +) -> anyhow::Result<()> { + // Open and read the file entirely + let mut src = File::open(stubfile.as_ref())?; + let mut data = String::new(); + src.read_to_string(&mut data)?; + drop(src); // Close the file early + + // Run the replace operation in memory + let mut new_string: String = string_to_find.to_owned(); + new_string.push_str(string_to_add_below); + let new_data = data.replace(string_to_find, &new_string); + + // Recreate the file and dump the processed contents to it + let mut dst = File::create(stubfile.as_ref())?; + dst.write_all(new_data.as_bytes())?; + + anyhow::Ok(()) +} + +/// Replaces items in the stubfile for when pyo3 cant do it +/// properly. +/// +/// This will: +/// * Open the created stubfile +/// * Find the line in `string_to_find` (fails if not found) +/// * Replace it with `string_to_replace` (fails if cannot) +/// +/// # Arguments +/// +/// * `stubfile` - `Path` representing filename of the stubfile to edit +/// +/// * `string_to_find` - string to find (which will be replaced) +/// +/// * `string_to_replace` - string to put in `string_to_find`s place +/// +/// +/// # Returns +/// +/// * Result Ok if stub file was modified successfully. +/// +/// +#[cfg(feature = "python")] +fn replace_stub>( + stubfile: P, + string_to_find: &str, + string_to_replace: &str, +) -> anyhow::Result<()> { + // Open and read the file entirely + let mut src = File::open(stubfile.as_ref())?; + let mut data = String::new(); + src.read_to_string(&mut data)?; + drop(src); // Close the file early + + // Run the replace operation in memory + let new_data = data.replace(string_to_find, string_to_replace); + + // Recreate the file and dump the processed contents to it + let mut dst = File::create(stubfile.as_ref())?; + dst.write_all(new_data.as_bytes())?; + + anyhow::Ok(()) +} + +#[test] +#[cfg(feature = "python")] +fn test_insert_stub_below() { + // Create ephemeral temp directory which will be deleted at end of test + let dir = + TempDir::new("test_insert_stub_below").expect("Cannot create temp directory for test"); + + // Create a test file + let text_filename = dir.path().join("test.pyi"); + + let mut test_file = File::create(&text_filename) + .unwrap_or_else(|_| panic!("Could not open {}", text_filename.to_str().unwrap())); + // Write some lines + let content = " Hello\n World\n 1234"; + let _ = test_file.write_all(content.as_bytes()); + + // Now run the add_stub command + insert_stub_below(&text_filename, " World\n", " added_string\n") + .unwrap_or_else(|_| panic!("Could not add_stub to {}", text_filename.to_str().unwrap())); + + // Now reread the file + let test_file = File::open(&text_filename) + .unwrap_or_else(|_| panic!("Could not open {}", text_filename.to_str().unwrap())); + let mut lines = Vec::new(); + + for line in std::io::read_to_string(test_file).unwrap().lines() { + lines.push(line.to_string()) + } + + // Remove our temp dir + dir.close().expect("Failed to close temp directory"); + + assert_eq!(lines[0], " Hello"); + assert_eq!(lines[1], " World"); + assert_eq!(lines[2], " added_string"); + assert_eq!(lines[3], " 1234"); +} + +#[test] +#[cfg(feature = "python")] +fn test_replace_stub() { + // Create ephemeral temp directory which will be deleted at end of test + let dir = + TempDir::new("mwalib_test_replace_stub").expect("Cannot create temp directory for test"); + + // Create a test file + let text_filename = dir.path().join("test.pyi"); + + let mut test_file = File::create(&text_filename) + .unwrap_or_else(|_| panic!("Could not open {}", text_filename.to_str().unwrap())); + // Write some lines + let content = " Hello\n World\n 1234"; + let _ = test_file.write_all(content.as_bytes()); + + // Now run the add_stub command + replace_stub( + &text_filename, + " World\n", + " This is the replaced string\n", + ) + .unwrap_or_else(|_| panic!("Could not add_stub to {}", text_filename.to_str().unwrap())); + + // Now reread the file + let test_file = File::open(&text_filename) + .unwrap_or_else(|_| panic!("Could not open {}", text_filename.to_str().unwrap())); + let mut lines = Vec::new(); + + for line in std::io::read_to_string(test_file).unwrap().lines() { + lines.push(line.to_string()) + } + + // Remove our temp dir + dir.close().expect("Failed to close temp directory"); + + assert_eq!(lines[0], " Hello"); + assert_eq!(lines[1], " This is the replaced string"); + assert_eq!(lines[2], " 1234"); +} diff --git a/examples/mwalib-sum-all-hdus.py b/examples/mwalib-sum-all-hdus.py index 945b4fa..44d8c8e 100644 --- a/examples/mwalib-sum-all-hdus.py +++ b/examples/mwalib-sum-all-hdus.py @@ -78,18 +78,13 @@ def sum_parallel_by_freq( if __name__ == "__main__": parser = argparse.ArgumentParser(description=f"Using mwalib {mwalib.__version__}") - parser.add_argument( - "-m", "--metafits", required=True, help="Path to the metafits file." - ) + parser.add_argument("-m", "--metafits", required=True, help="Path to the metafits file.") parser.add_argument("gpuboxes", nargs="*", help="Paths to the gpubox files.") args = parser.parse_args() # fast sum using all cores num_cores = os.cpu_count() - print( - f"Using {num_cores} cores to fast sum all hdus by baseline, then by" - " frequency..." - ) + print(f"Using {num_cores} cores to fast sum all hdus by baseline, then by" " frequency...") start_time_fast = time.time() processed_list = Parallel(n_jobs=num_cores)( @@ -101,8 +96,7 @@ def sum_parallel_by_freq( start_time_fast = time.time() processed_list = Parallel(n_jobs=num_cores)( - delayed(sum_parallel_by_freq)(args.metafits, args.gpuboxes, c) - for c in range(24) + delayed(sum_parallel_by_freq)(args.metafits, args.gpuboxes, c) for c in range(24) ) fast_sum = np.sum(processed_list) stop_time_fast = time.time() diff --git a/mwalib.pyi b/mwalib.pyi new file mode 100644 index 0000000..114868c --- /dev/null +++ b/mwalib.pyi @@ -0,0 +1,561 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401 + +import numpy +import numpy.typing +import typing +import datetime +from enum import Enum, auto + +__version__: str + +class CableDelaysApplied(Enum): + r""" + The type of cable delays applied to the data + """ + + NoCableDelaysApplied = auto() + CableAndRecClock = auto() + CableAndRecClockAndBeamformerDipoleDelays = auto() + +class GeometricDelaysApplied(Enum): + r""" + The type of geometric delays applied to the data + """ + + No = auto() + Zenith = auto() + TilePointing = auto() + AzElTracking = auto() + +class MWAMode(Enum): + r""" + The MODE the system was in for this observation + """ + + No_Capture = auto() + Burst_Vsib = auto() + Sw_Cor_Vsib = auto() + Hw_Cor_Pkts = auto() + Rts_32t = auto() + Hw_Lfiles = auto() + Hw_Lfiles_Nomentok = auto() + Sw_Cor_Vsib_Nomentok = auto() + Burst_Vsib_Synced = auto() + Burst_Vsib_Raw = auto() + Lfiles_Client = auto() + No_Capture_Burst = auto() + Enter_Burst = auto() + Enter_Channel = auto() + Voltage_Raw = auto() + Corr_Mode_Change = auto() + Voltage_Start = auto() + Voltage_Stop = auto() + Voltage_Buffer = auto() + Mwax_Correlator = auto() + Mwax_Vcs = auto() + Mwax_Buffer = auto() + +class MWAVersion(Enum): + r""" + Enum for all of the known variants of file format based on Correlator version + """ + + CorrOldLegacy = auto() + CorrLegacy = auto() + CorrMWAXv2 = auto() + VCSLegacyRecombined = auto() + VCSMWAXv2 = auto() + +class Pol(Enum): + r""" + Instrument polarisation. + """ + + X = auto() + Y = auto() + +class ReceiverType(Enum): + r""" + ReceiverType enum. + """ + + Unknown = auto() + RRI = auto() + NI = auto() + Pseudo = auto() + SHAO = auto() + EDA2 = auto() + +class VisPol(Enum): + r""" + Visibility polarisations + """ + + XX = auto() + XY = auto() + YX = auto() + YY = auto() + +class Rfinput: + r""" + Structure for storing MWA rf_chains (tile with polarisation) information from the metafits file + """ + + input: int + ant: int + tile_id: int + tile_name: str + pol: Pol + electrical_length_m: float + north_m: float + east_m: float + height_m: float + vcs_order: int + subfile_order: int + flagged: bool + digital_gains: list[float] + dipole_gains: list[float] + dipole_delays: list[int] + rec_number: int + rec_slot_number: int + rec_type: ReceiverType + flavour: str + has_whitening_filter: bool + calib_delay: typing.Optional[float] + calib_gains: typing.Optional[list[float]] + signal_chain_corrections_index: typing.Optional[int] + +class Antenna: + r""" + Structure for storing MWA antennas (tiles without polarisation) information from the metafits file + """ + + ant: int + tile_id: int + tile_name: str + rfinput_x: Rfinput + rfinput_y: Rfinput + electrical_length_m: float + north_m: float + east_m: float + height_m: float + +class Baseline: + r""" + This is a struct for our baselines, so callers know the antenna ordering + """ + + ant1_index: int + ant2_index: int + +class CoarseChannel: + r""" + This is a struct for coarse channels + """ + + corr_chan_number: int + rec_chan_number: int + gpubox_number: int + chan_width_hz: int + chan_start_hz: int + chan_centre_hz: int + chan_end_hz: int + +class GpuBoxFile: + r""" + This represents one gpubox file + """ + + filename: str + channel_identifier: int + +class GpuBoxBatch: + r""" + This represents one group of gpubox files with the same "batch" identitifer. + e.g. obsid_datetime_chan_batch + """ + + batch_number: int + gpubox_files: list[GpuBoxFile] + +class SignalChainCorrection: + r""" + Signal chain correction table + """ + + receiver_type: ReceiverType + whitening_filter: bool + corrections: list[float] + +class TimeStep: + r""" + 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 + """ + + unix_time_ms: int + gps_time_ms: int + +class MetafitsContext: + r""" + Metafits context. This represents the basic metadata for an MWA observation. + """ + + mwa_version: typing.Optional[MWAVersion] + obs_id: int + sched_start_gps_time_ms: int + sched_end_gps_time_ms: int + sched_start_unix_time_ms: int + sched_end_unix_time_ms: int + sched_start_utc: datetime.datetime + sched_end_utc: datetime.datetime + sched_start_mjd: float + sched_end_mjd: float + sched_duration_ms: int + dut1: typing.Optional[float] + ra_tile_pointing_degrees: float + dec_tile_pointing_degrees: float + ra_phase_center_degrees: typing.Optional[float] + dec_phase_center_degrees: typing.Optional[float] + az_deg: float + alt_deg: float + za_deg: float + az_rad: float + alt_rad: float + za_rad: float + sun_alt_deg: typing.Optional[float] + sun_distance_deg: typing.Optional[float] + moon_distance_deg: typing.Optional[float] + jupiter_distance_deg: typing.Optional[float] + lst_deg: float + lst_rad: float + hour_angle_string: str + grid_name: str + grid_number: int + creator: str + project_id: str + obs_name: str + mode: MWAMode + geometric_delays_applied: GeometricDelaysApplied + cable_delays_applied: CableDelaysApplied + calibration_delays_and_gains_applied: bool + corr_fine_chan_width_hz: int + corr_int_time_ms: int + corr_raw_scale_factor: float + num_corr_fine_chans_per_coarse: int + volt_fine_chan_width_hz: int + num_volt_fine_chans_per_coarse: int + receivers: list[int] + num_receivers: int + delays: list[int] + num_delays: int + calibrator: bool + calibrator_source: str + global_analogue_attenuation_db: float + quack_time_duration_ms: int + good_time_unix_ms: int + good_time_gps_ms: int + num_ants: int + antennas: list[Antenna] + num_rf_inputs: int + rf_inputs: list[Rfinput] + num_ant_pols: int + num_metafits_timesteps: int + metafits_timesteps: list[TimeStep] + num_metafits_coarse_chans: int + metafits_coarse_chans: list[CoarseChannel] + num_metafits_fine_chan_freqs: int + metafits_fine_chan_freqs_hz: list[float] + obs_bandwidth_hz: int + coarse_chan_width_hz: int + centre_freq_hz: int + num_baselines: int + baselines: list[Baseline] + num_visibility_pols: int + metafits_filename: str + oversampled: bool + deripple_applied: bool + deripple_param: str + best_cal_fit_id: typing.Optional[int] + best_cal_obs_id: typing.Optional[int] + best_cal_code_ver: typing.Optional[str] + best_cal_fit_timestamp: typing.Optional[str] + best_cal_creator: typing.Optional[str] + best_cal_fit_iters: typing.Optional[int] + best_cal_fit_iter_limit: typing.Optional[int] + signal_chain_corrections: typing.Optional[list[SignalChainCorrection]] + num_signal_chain_corrections: int + def __new__(cls, metafits_filename: str, mwa_version: typing.Optional[MWAVersion] = None) -> MetafitsContext: ... + def __repr__(self) -> str: ... + def __enter__(self) -> MetafitsContext: ... + def __exit__(self, _exc_type: typing.Any, _exc_value: typing.Any, _traceback: typing.Any) -> None: ... + +class CorrelatorContext: + r""" + This represents the basic metadata and methods for an MWA correlator observation. + """ + + metafits_context: MetafitsContext + mwa_version: MWAVersion + timesteps: list[TimeStep] + num_timesteps: int + coarse_chans: list[CoarseChannel] + num_coarse_chans: int + common_timestep_indices: list[int] + num_common_timesteps: int + common_coarse_chan_indices: list[int] + num_common_coarse_chans: int + common_start_unix_time_ms: int + common_end_unix_time_ms: int + common_start_gps_time_ms: int + common_end_gps_time_ms: int + common_duration_ms: int + common_bandwidth_hz: int + common_good_timestep_indices: list[int] + num_common_good_timesteps: int + common_good_coarse_chan_indices: list[int] + num_common_good_coarse_chans: int + common_good_start_unix_time_ms: int + common_good_end_unix_time_ms: int + common_good_start_gps_time_ms: int + common_good_end_gps_time_ms: int + common_good_duration_ms: int + common_good_bandwidth_hz: int + provided_timestep_indices: list[int] + num_provided_timesteps: int + provided_coarse_chan_indices: list[int] + num_provided_coarse_chans: int + num_timestep_coarse_chan_bytes: int + num_timestep_coarse_chan_floats: int + num_timestep_coarse_chan_weight_floats: int + num_gpubox_files: int + gpubox_batches: list[GpuBoxBatch] + gpubox_time_map: dict[int, dict[int, tuple[int, int]]] + 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""" + For a given list of correlator coarse channel indices, return a list of the center frequencies for all the fine channels in the given coarse channels + + Args: + corr_coarse_chan_indices (list[int]): a list containing correlator coarse channel indices for which you want fine channels for. Does not need to be contiguous. + + Returns: + fine_chan_freqs_hz_array (list[float]): a vector of floats containing the centre sky frequencies of all the fine channels for the given coarse channels. + """ + ... + + def read_by_baseline( + self, corr_timestep_index: int, corr_coarse_chan_index: int + ) -> numpy.typing.NDArray[numpy.float32]: + r""" + Read a single timestep for a single coarse channel. The output visibilities are in order: baseline,frequency,pol,r,i + + Args: + corr_timestep_index (int): index within the CorrelatorContext timestep array for the desired timestep. This corresponds to the element within CorrelatorContext.timesteps. + corr_coarse_chan_index (int): index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds to the element within CorrelatorContext.coarse_chans. + + Returns: + data (numpy.typing.NDArray[numpy.float32]): 3 dimensional ndarray of 32 bit floats containing the data in [baseline],[frequency],[pol,r,i] order, if Ok. + """ + ... + + def read_by_frequency( + self, corr_timestep_index: int, corr_coarse_chan_index: int + ) -> numpy.typing.NDArray[numpy.float32]: + r""" + Read a single timestep for a single coarse channel. The output visibilities are in order: frequency,baseline,pol,r,i + + Args: + corr_timestep_index (int): index within the CorrelatorContext timestep array for the desired timestep. This corresponds to the element within CorrelatorContext.timesteps. + corr_coarse_chan_index (int): index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds to the element within CorrelatorContext.coarse_chans. + + Returns: + data (numpy.typing.NDArray[numpy.float32]): 3 dimensional ndarray of 32 bit floats containing the data in [frequency],[baseline],[pol,r,i] order, if Ok. + """ + ... + + def read_weights_by_baseline( + self, corr_timestep_index: int, corr_coarse_chan_index: int + ) -> numpy.typing.NDArray[numpy.float32]: + r""" + Read weights for a single timestep for a single coarse channel. The output weights are in order: baseline,pol + + Args: + corr_timestep_index (int): index within the CorrelatorContext timestep array for the desired timestep. This corresponds to the element within CorrelatorContext.timesteps. + corr_coarse_chan_index (int): index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds to the element within CorrelatorContext.coarse_chans. + + Returns: + data (numpy.typing.NDArray[numpy.float32]): A 2 dimensional ndarray of 32 bit floats containing the data in [baseline],[pol] order, if Ok. + """ + ... + + def __repr__(self) -> str: ... + def __enter__(self) -> CorrelatorContext: ... + def __exit__(self, _exc_type: typing.Any, _exc_value: typing.Any, _traceback: typing.Any) -> None: ... + +class VoltageFile: + r""" + This represents one voltage file + """ + + filename: str + channel_identifier: int + +class VoltageFileBatch: + r""" + This represents one group of voltage files with the same "batch" identitifer (gps time). + e.g. + MWA Legacy: obsid_gpstime_datetime_chan + MWAX : obsid_gpstime_datetime_chan + """ + + gps_time_seconds: int + voltage_files: list[VoltageFile] + +class VoltageContext: + r""" + This represents the basic metadata and methods for an MWA voltage capture system (VCS) observation. + """ + + metafits_context: MetafitsContext + mwa_version: MWAVersion + timesteps: list[TimeStep] + num_timesteps: int + timestep_duration_ms: int + coarse_chans: list[CoarseChannel] + num_coarse_chans: int + common_timestep_indices: list[int] + num_common_timesteps: int + common_coarse_chan_indices: list[int] + num_common_coarse_chans: int + common_start_unix_time_ms: int + common_end_unix_time_ms: int + common_start_gps_time_ms: int + common_end_gps_time_ms: int + common_duration_ms: int + common_bandwidth_hz: int + common_good_timestep_indices: list[int] + num_common_good_timesteps: int + common_good_coarse_chan_indices: list[int] + num_common_good_coarse_chans: int + common_good_start_unix_time_ms: int + common_good_end_unix_time_ms: int + common_good_start_gps_time_ms: int + common_good_end_gps_time_ms: int + common_good_duration_ms: int + common_good_bandwidth_hz: int + provided_timestep_indices: list[int] + num_provided_timesteps: int + provided_coarse_chan_indices: list[int] + num_provided_coarse_chans: int + coarse_chan_width_hz: int + fine_chan_width_hz: int + num_fine_chans_per_coarse: int + sample_size_bytes: int + num_voltage_blocks_per_timestep: int + num_voltage_blocks_per_second: int + num_samples_per_voltage_block: int + voltage_block_size_bytes: int + delay_block_size_bytes: int + data_file_header_size_bytes: int + expected_voltage_data_file_size_bytes: int + voltage_batches: list[VoltageFileBatch] + def __new__(cls, metafits_filename: str, voltage_filenames: list[str]) -> VoltageContext: ... + def get_fine_chan_freqs_hz_array(self, volt_coarse_chan_indices: typing.Sequence[int]) -> list[float]: + r""" + For a given list of voltage coarse channel indices, return a list of the center frequencies for all the fine channels in the given coarse channels. + + Args: + volt_coarse_chan_indices (list[int]): a list containing correlator coarse channel indices for which you want fine channels for. Does not need to be contiguous. + + Returns: + fine_chan_freqs_hz_array (list[float]): a vector of floats containing the centre sky frequencies of all the fine channels for the given coarse channels. + """ + ... + + def read_file(self, volt_timestep_index: int, volt_coarse_chan_index: int) -> numpy.typing.NDArray[numpy.int8]: + r""" + Read a single timestep / coarse channel worth of data + + Args: + volt_timestep_index (int): index within the timestep array for the desired timestep. This corresponds to the element within VoltageContext.timesteps. For mwa legacy each index represents 1 second increments, for mwax it is 8 second increments. + volt_coarse_chan_index (int): index within the coarse_chan array for the desired coarse channel. This corresponds to the element within VoltageContext.coarse_chans. + + Returns: + data (numpy.typing.NDArray[numpy.int8]): A 6 dimensional ndarray of signed bytes containing the data, if Ok. + + NOTE: The shape of the ndarray is different between LegacyVCS and MWAX VCS + Legacy: [second],[time sample],[chan],[ant],[pol],[complexity] + where complexity is a byte (first 4 bits for real, second 4 bits for imaginary) in 2's compliment + MWAX : [second],[voltage_block],[antenna],[pol],[sample],[r,i] + """ + ... + + def read_second( + self, gps_second_start: int, gps_second_count: int, volt_coarse_chan_index: int + ) -> numpy.typing.NDArray[numpy.int8]: + r""" + Read a single or multiple seconds of data for a coarse channel + + Args: + gps_second_start (int): GPS second within the observation to start returning data. + gps_second_count (int): number of seconds of data to return. + volt_coarse_chan_index (int): index within the coarse_chan array for the desired coarse channel. This corresponds to the element within VoltageContext.coarse_chans. + + Returns: + data (numpy.typing.NDArray[numpy.int8]): A 6 dimensional ndarray of signed bytes containing the data, if Ok. + + NOTE: The shape is different between LegacyVCS and MWAX VCS + Legacy: [second],[time sample],[chan],[ant],[pol],[complexity] + where complexity is a byte (first 4 bits for real, second 4 bits for imaginary) in 2's compliment + MWAX : [second],[voltage_block],[antenna],[pol],[sample],[r,i] + """ + ... + + def __repr__(self) -> str: ... + def __enter__(self) -> VoltageContext: ... + def __exit__(self, _exc_type: typing.Any, _exc_value: typing.Any, _traceback: typing.Any) -> None: ... + +class GpuboxErrorBatchMissing(Exception): ... +class GpuboxErrorCorrVerMismatch(Exception): ... +class GpuboxErrorEmptyBTreeMap(Exception): ... +class GpuboxErrorFits(Exception): ... +class GpuboxErrorInvalidCoarseChanIndex(Exception): ... +class GpuboxErrorInvalidMwaVersion(Exception): ... +class GpuboxErrorInvalidTimeStepIndex(Exception): ... +class GpuboxErrorLegacyNaxis1Mismatch(Exception): ... +class GpuboxErrorLegacyNaxis2Mismatch(Exception): ... +class GpuboxErrorMissingObsid(Exception): ... +class GpuboxErrorMixture(Exception): ... +class GpuboxErrorMwaxCorrVerMismatch(Exception): ... +class GpuboxErrorMwaxCorrVerMissing(Exception): ... +class GpuboxErrorMwaxNaxis1Mismatch(Exception): ... +class GpuboxErrorMwaxNaxis2Mismatch(Exception): ... +class GpuboxErrorNoDataForTimeStepCoarseChannel(Exception): ... +class GpuboxErrorNoDataHDUsInGpuboxFile(Exception): ... +class GpuboxErrorNoGpuboxes(Exception): ... +class GpuboxErrorObsidMismatch(Exception): ... +class GpuboxErrorUnequalHduSizes(Exception): ... +class GpuboxErrorUnevenCountInBatches(Exception): ... +class GpuboxErrorUnrecognised(Exception): ... +class MwalibError(Exception): ... +class VoltageErro(Exception): ... +class VoltageErrorEmptyBTreeMap(Exception): ... +class VoltageErrorGpsTimeMissing(Exception): ... +class VoltageErrorInvalidBufferSize(Exception): ... +class VoltageErrorInvalidCoarseChanIndex(Exception): ... +class VoltageErrorInvalidGpsSecondCount(Exception): ... +class VoltageErrorInvalidGpsSecondStart(Exception): ... +class VoltageErrorInvalidMwaVersion(Exception): ... +class VoltageErrorInvalidTimeStepIndex(Exception): ... +class VoltageErrorInvalidVoltageFileSize(Exception): ... +class VoltageErrorMetafitsObsidMismatch(Exception): ... +class VoltageErrorMissingObsid(Exception): ... +class VoltageErrorMixture(Exception): ... +class VoltageErrorNoDataForTimeStepCoarseChannel(Exception): ... +class VoltageErrorNoVoltageFiles(Exception): ... +class VoltageErrorObsidMismatch(Exception): ... +class VoltageErrorUnequalFileSizes(Exception): ... +class VoltageErrorUnevenChannelsForGpsTime(Exception): ... +class VoltageErrorUnrecognised(Exception): ... diff --git a/src/antenna/mod.rs b/src/antenna/mod.rs index 7efef88..29f0eed 100644 --- a/src/antenna/mod.rs +++ b/src/antenna/mod.rs @@ -7,17 +7,21 @@ use crate::rfinput::*; use std::fmt; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; + #[cfg(test)] 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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct Antenna { /// This is the antenna number. /// Nominally this is the field we sort by to get the desired output order of antenna. /// X and Y have the same antenna number. This is the sorted ordinal order of the antenna.None - /// e.g. 0...N-1 + /// e.g. 0...N-1 pub ant: u32, /// Numeric part of tile_name for the antenna. Each pol has the same value /// e.g. tile_name "tile011" hsa tile_id of 11 diff --git a/src/baseline/mod.rs b/src/baseline/mod.rs index f56949e..714043f 100644 --- a/src/baseline/mod.rs +++ b/src/baseline/mod.rs @@ -7,15 +7,19 @@ use crate::misc; use std::fmt; +#[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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct Baseline { - /// Index in the mwalibContext.antenna array for antenna1 for this baseline + /// Index in the mwalibContext.antenna array for antenna1 for this baseline pub ant1_index: usize, - /// Index in the mwalibContext.antenna array for antenna2 for this baseline + /// Index in the mwalibContext.antenna array for antenna2 for this baseline pub ant2_index: usize, } diff --git a/src/coarse_channel/mod.rs b/src/coarse_channel/mod.rs index b5f304c..2388fad 100644 --- a/src/coarse_channel/mod.rs +++ b/src/coarse_channel/mod.rs @@ -11,12 +11,16 @@ use crate::*; use error::CoarseChannelError; use std::fmt; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; + #[cfg(test)] 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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct CoarseChannel { /// Correlator channel is 0 indexed (0..N-1) pub corr_chan_number: usize, @@ -105,7 +109,7 @@ impl CoarseChannel { /// /// # Arguments /// - /// `metafits_fptr` - a reference to a metafits FitsFile object. + /// `metafits_fptr` - a reference to a metafits MWAFitsFile object. /// /// `metafits_hdu` - a reference to a metafits primary HDU. /// @@ -118,7 +122,7 @@ impl CoarseChannel { /// The width in Hz of each coarse channel /// pub(crate) fn get_metafits_coarse_channel_info( - metafits_fptr: &mut fitsio::FitsFile, + metafits_fptr: &mut MWAFitsFile, hdu: &fitsio::hdu::FitsHdu, observation_bandwidth_hz: u32, ) -> Result<(Vec, u32), FitsError> { diff --git a/src/convert/mod.rs b/src/convert/mod.rs index 2ec930e..7ea5c2c 100644 --- a/src/convert/mod.rs +++ b/src/convert/mod.rs @@ -36,7 +36,7 @@ fn fine_pfb_reorder(input: usize) -> usize { /// Structure for storing where in the input visibilities to get the specified baseline when converting #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass)] +#[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 0d92011..816f4b5 100644 --- a/src/correlator_context/mod.rs +++ b/src/correlator_context/mod.rs @@ -16,6 +16,9 @@ use crate::metafits_context::*; use crate::timestep::*; use crate::*; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; + #[cfg(feature = "python")] mod python; @@ -25,8 +28,9 @@ 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))] #[derive(Debug)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct CorrelatorContext { /// Observation Metadata obtained from the metafits file pub metafits_context: MetafitsContext, @@ -117,7 +121,7 @@ pub struct CorrelatorContext { /// number, batch number and HDU index are everything needed to find the /// correct HDU out of all gpubox files. pub gpubox_time_map: BTreeMap>, - /// A conversion table to optimise reading of legacy MWA HDUs + /// A conversion table to optimise reading of legacy MWA HDUs pub(crate) legacy_conversion_table: Vec, } @@ -782,7 +786,7 @@ impl CorrelatorContext { /// /// * `visibility_pols` - the number of pols produced by the correlator (always 4 for MWA) /// - /// * `gpubox_fptr` - FITSFile pointer to an MWA GPUbox file + /// * `gpubox_fptr` - MWAFITSFile pointer to an MWA GPUbox file /// /// # Returns /// @@ -794,7 +798,7 @@ impl CorrelatorContext { metafits_fine_chans_per_coarse: usize, metafits_baselines: usize, visibility_pols: usize, - gpubox_fptr: &mut fitsio::FitsFile, + gpubox_fptr: &mut MWAFitsFile, ) -> Result<(), GpuboxError> { // Get NAXIS1 and NAXIS2 from a gpubox file first image HDU let hdu = fits_open_hdu!(gpubox_fptr, 1)?; diff --git a/src/correlator_context/python.rs b/src/correlator_context/python.rs index 2723f71..980663e 100644 --- a/src/correlator_context/python.rs +++ b/src/correlator_context/python.rs @@ -16,76 +16,55 @@ use numpy::PyArray2; use numpy::PyArray3; #[cfg(feature = "python")] use pyo3::prelude::*; - #[cfg(feature = "python")] -#[pymethods] +use pyo3_stub_gen_derive::gen_stub_pymethods; + +#[cfg_attr(feature = "python", gen_stub_pymethods)] +#[cfg_attr(feature = "python", pymethods)] impl CorrelatorContext { - /// From a path to a metafits file and paths to gpubox files, create an `CorrelatorContext`. - /// - /// # Arguments - /// - /// * `metafits_filename` - filename of metafits file as a path or string. - /// - /// * `gpubox_filenames` - list of filenames of gpubox files as paths or strings. - /// - /// - /// # Returns + /// From a path to a metafits file and paths to gpubox files, create a `CorrelatorContext`. /// - /// * A populated CorrelatorContext object if Ok. + /// Args: + /// metafits_filename (str): filename of metafits file as a path or string. + /// gpubox_filenames (list[str]): list of filenames of gpubox files. /// + /// Returns: + /// correlator_context (CorelatorContext): a populated CorrelatorContext object if Ok. #[new] - #[pyo3(signature = (metafits_filename, gpubox_filenames))] - fn pyo3_new(metafits_filename: PyObject, gpubox_filenames: Vec) -> PyResult { + #[pyo3(signature = (metafits_filename, gpubox_filenames), text_signature = "(metafits_filename: str, mwa_version: list[gpubox_filenames])")] + fn pyo3_new(metafits_filename: &str, gpubox_filenames: Vec) -> PyResult { // Convert the gpubox filenames. let gpubox_filenames: Vec = gpubox_filenames .into_iter() .map(|g| g.to_string()) .collect(); - let c = CorrelatorContext::new(metafits_filename.to_string(), &gpubox_filenames)?; + let c = CorrelatorContext::new(metafits_filename, &gpubox_filenames)?; Ok(c) } - /// For a given list of correlator coarse channel indices, return a list of the center - /// frequencies for all the fine channels in the given coarse channels + /// For a given list of correlator coarse channel indices, return a list of the center frequencies for all the fine channels in the given coarse channels /// - /// # Arguments + /// Args: + /// corr_coarse_chan_indices (list[int]): a list containing correlator coarse channel indices for which you want fine channels for. Does not need to be contiguous. /// - /// * `corr_coarse_chan_indices` - a list containing correlator coarse channel indices - /// for which you want fine channels for. Does not need to be - /// contiguous. - /// - /// # Returns - /// - /// * a vector of floats containing the centre sky frequencies of all the fine channels for the - /// given coarse channels. - /// - #[pyo3( - name = "get_fine_chan_freqs_hz_array", - signature = (corr_coarse_chan_indices) - )] + /// Returns: + /// fine_chan_freqs_hz_array (list[float]): a vector of floats containing the centre sky frequencies of all the fine channels for the given coarse channels. + #[pyo3(name = "get_fine_chan_freqs_hz_array")] fn pyo3_get_fine_chan_freqs_hz_array(&self, corr_coarse_chan_indices: Vec) -> Vec { self.get_fine_chan_freqs_hz_array(&corr_coarse_chan_indices) } - /// Read a single timestep for a single coarse channel - /// The output visibilities are in order: - /// baseline,frequency,pol,r,i - /// - /// # Arguments - /// - /// * `corr_timestep_index` - index within the CorrelatorContext timestep array for the desired timestep. This corresponds - /// to the element within CorrelatorContext.timesteps. + /// Read a single timestep for a single coarse channel. The output visibilities are in order: baseline,frequency,pol,r,i /// - /// * `corr_coarse_chan_index` - index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds - /// to the element within CorrelatorContext.coarse_chans. - /// - /// # Returns - /// - /// * An ndarray of 32 bit floats containing the data in [baseline][frequency][pol,r,i] order, if Ok. + /// Args: + /// corr_timestep_index (int): index within the CorrelatorContext timestep array for the desired timestep. This corresponds to the element within CorrelatorContext.timesteps. + /// corr_coarse_chan_index (int): index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds to the element within CorrelatorContext.coarse_chans. /// + /// Returns: + /// data (numpy.typing.NDArray[numpy.float32]): 3 dimensional ndarray of 32 bit floats containing the data in [baseline],[frequency],[pol,r,i] order, if Ok. #[pyo3( name = "read_by_baseline", - signature = (corr_timestep_index, corr_coarse_chan_index) + text_signature = "(self, corr_timestep_index, corr_coarse_chan_index)" )] fn pyo3_read_by_baseline<'py>( &self, @@ -110,26 +89,17 @@ impl CorrelatorContext { Ok(data) } - /// Read a single timestep for a single coarse channel - /// The output visibilities are in order: - /// frequency,baseline,pol,r,i - /// - /// # Arguments - /// - /// * `corr_timestep_index` - index within the CorrelatorContext timestep array for the desired timestep. This corresponds - /// to the element within CorrelatorContext.timesteps. - /// - /// * `corr_coarse_chan_index` - index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds - /// to the element within CorrelatorContext.coarse_chans. - /// + /// Read a single timestep for a single coarse channel. The output visibilities are in order: frequency,baseline,pol,r,i /// - /// # Returns - /// - /// * An ndarray of 32 bit floats containing the data in [frequency],[baseline],[pol,r,i] order, if Ok. + /// Args: + /// corr_timestep_index (int): index within the CorrelatorContext timestep array for the desired timestep. This corresponds to the element within CorrelatorContext.timesteps. + /// corr_coarse_chan_index (int): index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds to the element within CorrelatorContext.coarse_chans. /// + /// Returns: + /// data (numpy.typing.NDArray[numpy.float32]): 3 dimensional ndarray of 32 bit floats containing the data in [frequency],[baseline],[pol,r,i] order, if Ok. #[pyo3( name = "read_by_frequency", - signature = (corr_timestep_index, corr_coarse_chan_index) + text_signature = "(self, corr_timestep_index, corr_coarse_chan_index)" )] fn pyo3_read_by_frequency<'py>( &self, @@ -154,26 +124,17 @@ impl CorrelatorContext { Ok(data) } - /// Read weights for a single timestep for a single coarse channel - /// The output weights are in order: - /// baseline,pol - /// - /// # Arguments - /// - /// * `corr_timestep_index` - index within the CorrelatorContext timestep array for the desired timestep. This corresponds - /// to the element within CorrelatorContext.timesteps. - /// - /// * `corr_coarse_chan_index` - index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds - /// to the element within CorrelatorContext.coarse_chans. - /// - /// - /// # Returns + /// Read weights for a single timestep for a single coarse channel. The output weights are in order: baseline,pol /// - /// * An ndarray of 32 bit floats containing the data in [baseline][pol] order, if Ok. + /// Args: + /// corr_timestep_index (int): index within the CorrelatorContext timestep array for the desired timestep. This corresponds to the element within CorrelatorContext.timesteps. + /// corr_coarse_chan_index (int): index within the CorrelatorContext coarse_chan array for the desired coarse channel. This corresponds to the element within CorrelatorContext.coarse_chans. /// + /// Returns: + /// data (numpy.typing.NDArray[numpy.float32]): A 2 dimensional ndarray of 32 bit floats containing the data in [baseline],[pol] order, if Ok. #[pyo3( name = "read_weights_by_baseline", - signature = (corr_timestep_index, corr_coarse_chan_index) + text_signature = "(self, corr_timestep_index, corr_coarse_chan_index)" )] fn pyo3_read_weights_by_baseline<'py>( &self, @@ -202,6 +163,7 @@ impl CorrelatorContext { format!("{}", self) } + #[pyo3()] fn __enter__(slf: Py) -> Py { slf } diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index e7a9002..0d216fc 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -1856,25 +1856,22 @@ pub unsafe extern "C" fn mwalib_metafits_metadata_get( // Populate signal chain corrections let mut signal_chain_corrections_vec: Vec = Vec::new(); - match &metafits_context.signal_chain_corrections { - Some(v) => { - for item in v.iter() { - let out_item = { - let metafits_context::SignalChainCorrection { - receiver_type, - whitening_filter, - corrections, - } = item; - ffi::SignalChainCorrection { - receiver_type: *receiver_type, - whitening_filter: *whitening_filter, - corrections: ffi_array_to_boxed_slice(corrections.clone()), - } - }; - signal_chain_corrections_vec.push(out_item); - } + if let Some(v) = &metafits_context.signal_chain_corrections { + for item in v.iter() { + let out_item = { + let signal_chain_correction::SignalChainCorrection { + receiver_type, + whitening_filter, + corrections, + } = item; + ffi::SignalChainCorrection { + receiver_type: *receiver_type, + whitening_filter: *whitening_filter, + corrections: ffi_array_to_boxed_slice(corrections.clone()), + } + }; + signal_chain_corrections_vec.push(out_item); } - None => {} } // Populate the outgoing structure with data from the metafits context diff --git a/src/fits_read/mod.rs b/src/fits_read/mod.rs index cbbd14d..8d84d39 100644 --- a/src/fits_read/mod.rs +++ b/src/fits_read/mod.rs @@ -5,8 +5,8 @@ //! Helper functions for reading FITS files. pub(crate) mod error; +pub use crate::mwa_fits_file::MWAFitsFile; pub use error::FitsError; - use fitsio::{hdu::*, FitsFile}; use log::trace; use std::ffi::*; @@ -293,14 +293,13 @@ pub fn _open_fits>( file: T, source_file: &'static str, source_line: u32, -) -> Result { - match FitsFile::open(file.as_ref()) { +) -> Result { + match FitsFile::open(&file) { Ok(f) => { - trace!( - "_open_fits() filename: '{}'", - file.as_ref().to_str().unwrap().to_string() - ); - Ok(f) + trace!("_open_fits() filename: '{}'", file.as_ref().display()); + + // Create the wrapper/MWAFitsFile + Ok(MWAFitsFile::new(file.as_ref().to_path_buf(), f)) } Err(e) => Err(FitsError::Open { fits_error: e, @@ -316,12 +315,12 @@ pub fn _open_fits>( /// To only be used internally; use the `fits_open_hdu!` macro instead. #[doc(hidden)] pub fn _open_hdu( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu_num: usize, source_file: &'static str, source_line: u32, ) -> Result { - match fits_fptr.hdu(hdu_num) { + match fits_fptr.fits_file.hdu(hdu_num) { Ok(f) => { trace!( "_open_hdu() filename: '{}' hdu: {}", @@ -345,12 +344,12 @@ pub fn _open_hdu( /// To only be used internally; use the `fits_open_hdu_by_name!` macro instead. #[doc(hidden)] pub fn _open_hdu_by_name( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu_name: &'static str, source_file: &'static str, source_line: u32, ) -> Result { - match fits_fptr.hdu(hdu_name) { + match fits_fptr.fits_file.hdu(hdu_name) { Ok(f) => { trace!( "_open_hdu_by_name() filename: '{}' hdu: {}", @@ -381,13 +380,13 @@ pub fn _open_hdu_by_name( // #[doc(hidden)] pub fn _get_optional_fits_key( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, keyword: &str, source_file: &'static str, source_line: u32, ) -> Result, FitsError> { - let unparsed_value: String = match hdu.read_key(fits_fptr, keyword) { + let unparsed_value: String = match hdu.read_key(&mut fits_fptr.fits_file, keyword) { Ok(key_value) => key_value, Err(e) => match &e { fitsio::errors::Error::Fits(fe) => match fe.status { @@ -439,7 +438,7 @@ pub fn _get_optional_fits_key( /// To only be used internally; use the `get_required_fits_key!` macro instead. #[doc(hidden)] pub fn _get_required_fits_key( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, keyword: &str, source_file: &'static str, @@ -463,13 +462,13 @@ pub fn _get_required_fits_key( /// To only be used internally; use the `fits_get_col!` macro instead. #[doc(hidden)] pub fn _get_fits_col( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, keyword: &str, source_file: &'static str, source_line: u32, ) -> Result, FitsError> { - match hdu.read_col(fits_fptr, keyword) { + match hdu.read_col(&mut fits_fptr.fits_file, keyword) { Ok(c) => { trace!( "_get_fits_col() filename: '{}' hdu: {} keyword: '{}' values: {}", @@ -507,7 +506,7 @@ pub fn _get_fits_col( /// This function calls cfitsio. Anything goes! #[doc(hidden)] pub fn _get_optional_fits_key_long_string( - fits_fptr: &mut fitsio::FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, keyword: &str, source_file: &'static str, @@ -520,7 +519,7 @@ pub fn _get_optional_fits_key_long_string( let mut status = 0; let mut long_string_ptr = ptr::null_mut(); fitsio_sys::ffgkls( - fits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), keyword_ffi.as_ptr(), &mut long_string_ptr, ptr::null_mut(), @@ -567,7 +566,7 @@ pub fn _get_optional_fits_key_long_string( /// macro instead. #[doc(hidden)] pub fn _get_required_fits_key_long_string( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, keyword: &str, source_file: &'static str, @@ -591,7 +590,7 @@ pub fn _get_required_fits_key_long_string( /// To only be used internally; use the `get_hdu_image_size!` macro instead. #[doc(hidden)] pub fn _get_hdu_image_size( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, source_file: &'static str, source_line: u32, @@ -620,13 +619,13 @@ pub fn _get_hdu_image_size( /// To only be used internally; use the `get_fits_image!` macro instead. #[doc(hidden)] pub fn _get_fits_image( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, source_file: &'static str, source_line: u32, ) -> Result { match &hdu.info { - HduInfo::ImageInfo { .. } => match hdu.read_image(fits_fptr) { + HduInfo::ImageInfo { .. } => match hdu.read_image(&mut fits_fptr.fits_file) { Ok(img) => { trace!( "_get_fits_image() filename: '{}' hdu: {}", @@ -655,7 +654,7 @@ pub fn _get_fits_image( /// Direct read of FITS HDU #[doc(hidden)] pub fn _get_fits_float_img_into_buf( - fits_fptr: &mut FitsFile, + fits_fptr: &mut MWAFitsFile, hdu: &FitsHdu, buffer: &mut [f32], source_file: &'static str, @@ -669,7 +668,7 @@ pub fn _get_fits_float_img_into_buf( // Call the underlying cfitsio read function for floats let mut status = 0; fitsio_sys::ffgpv( - fits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), fitsio_sys::TFLOAT as _, 1, buffer_len, @@ -704,25 +703,25 @@ pub fn _get_fits_float_img_into_buf( } pub fn read_cell_value( - metafits_fptr: &mut fitsio::FitsFile, - metafits_tile_table_hdu: &fitsio::hdu::FitsHdu, + fits_fptr: &mut MWAFitsFile, + fits_tile_table_hdu: &fitsio::hdu::FitsHdu, col_name: &str, row: usize, ) -> Result { - match metafits_tile_table_hdu.read_cell_value(metafits_fptr, col_name, row) { + match fits_tile_table_hdu.read_cell_value(&mut fits_fptr.fits_file, col_name, row) { Ok(c) => { trace!( "read_cell_value() filename: '{}' hdu: {} col_name: '{}' row '{}'", - metafits_fptr.filename.display(), - metafits_tile_table_hdu.number, + fits_fptr.filename.display(), + fits_tile_table_hdu.number, col_name, row ); Ok(c) } Err(_) => Err(FitsError::ReadCell { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_tile_table_hdu.number + 1, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_tile_table_hdu.number + 1, row_num: row, col_name: col_name.to_string(), }), @@ -731,8 +730,8 @@ pub fn read_cell_value( /// Pull out the array-in-a-cell values. T pub fn read_cell_array_u32( - metafits_fptr: &mut fitsio::FitsFile, - metafits_table_hdu: &fitsio::hdu::FitsHdu, + fits_fptr: &mut MWAFitsFile, + fits_table_hdu: &fitsio::hdu::FitsHdu, col_name: &str, row: i64, n_elem: usize, @@ -743,7 +742,7 @@ pub fn read_cell_array_u32( let mut col_num = -1; let keyword = std::ffi::CString::new(col_name).unwrap().into_raw(); fitsio_sys::ffgcno( - metafits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), 0, keyword, &mut col_num, @@ -752,8 +751,8 @@ pub fn read_cell_array_u32( // Check the status. if status != 0 { return Err(FitsError::CellArray { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_table_hdu.number, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_table_hdu.number, row_num: row, col_name: col_name.to_string(), }); @@ -767,7 +766,7 @@ pub fn read_cell_array_u32( array.shrink_to_fit(); let array_ptr = array.as_mut_ptr(); fitsio_sys::ffgcv( - metafits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), 31, col_num, row + 1, @@ -787,8 +786,8 @@ pub fn read_cell_array_u32( trace!( "read_cell_array_u32() filename: '{}' hdu: {} col_name: '{}' row '{}'", - metafits_fptr.filename.display(), - metafits_table_hdu.number, + fits_fptr.filename.display(), + fits_table_hdu.number, col_name, row ); @@ -799,15 +798,15 @@ pub fn read_cell_array_u32( println!( "ERROR {} read_cell_array_u32() filename: '{}' hdu: {} col_name: '{}' row '{}'", status, - metafits_fptr.filename.display(), - metafits_table_hdu.number, + fits_fptr.filename.display(), + fits_table_hdu.number, col_name, row ); Err(FitsError::CellArray { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_table_hdu.number + 1, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_table_hdu.number + 1, row_num: row, col_name: col_name.to_string(), }) @@ -820,8 +819,8 @@ pub fn read_cell_array_u32( /// datatype is f32, and that the fits datatype is E (f32), so it is not to be used /// generally! pub fn read_cell_array_f32( - metafits_fptr: &mut fitsio::FitsFile, - metafits_table_hdu: &fitsio::hdu::FitsHdu, + fits_fptr: &mut MWAFitsFile, + fits_table_hdu: &fitsio::hdu::FitsHdu, col_name: &str, row: i64, n_elem: usize, @@ -832,7 +831,7 @@ pub fn read_cell_array_f32( let mut col_num = -1; let keyword = std::ffi::CString::new(col_name).unwrap().into_raw(); fitsio_sys::ffgcno( - metafits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), 0, keyword, &mut col_num, @@ -841,8 +840,8 @@ pub fn read_cell_array_f32( // Check the status. if status != 0 { return Err(FitsError::CellArray { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_table_hdu.number, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_table_hdu.number, row_num: row, col_name: col_name.to_string(), }); @@ -860,7 +859,7 @@ pub fn read_cell_array_f32( //let nullval_ptr: *mut c_void = &mut null_replace_value as *mut f32 as *mut c_void; fitsio_sys::ffgcv( - metafits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), 42, col_num, row + 1, @@ -877,8 +876,8 @@ pub fn read_cell_array_f32( 0 => { trace!( "read_cell_array_f32() filename: '{}' hdu: {} col_name: '{}' row '{}'", - metafits_fptr.filename.display(), - metafits_table_hdu.number, + fits_fptr.filename.display(), + fits_table_hdu.number, col_name, row ); @@ -890,15 +889,15 @@ pub fn read_cell_array_f32( println!( "ERROR {} read_cell_array_f32() filename: '{}' hdu: {} col_name: '{}' row '{}'", status, - metafits_fptr.filename.display(), - metafits_table_hdu.number, + fits_fptr.filename.display(), + fits_table_hdu.number, col_name, row ); Err(FitsError::CellArray { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_table_hdu.number + 1, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_table_hdu.number + 1, row_num: row, col_name: col_name.to_string(), }) @@ -911,8 +910,8 @@ pub fn read_cell_array_f32( /// datatype is f64, and that the fits datatype is D (f64), so it is not to be used /// generally! pub fn read_cell_array_f64( - metafits_fptr: &mut fitsio::FitsFile, - metafits_table_hdu: &fitsio::hdu::FitsHdu, + fits_fptr: &mut MWAFitsFile, + fits_table_hdu: &fitsio::hdu::FitsHdu, col_name: &str, row: i64, n_elem: usize, @@ -923,7 +922,7 @@ pub fn read_cell_array_f64( let mut col_num = -1; let keyword = std::ffi::CString::new(col_name).unwrap().into_raw(); fitsio_sys::ffgcno( - metafits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), 0, keyword, &mut col_num, @@ -932,8 +931,8 @@ pub fn read_cell_array_f64( // Check the status. if status != 0 { return Err(FitsError::CellArray { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_table_hdu.number, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_table_hdu.number, row_num: row, col_name: col_name.to_string(), }); @@ -951,7 +950,7 @@ pub fn read_cell_array_f64( //let nullval_ptr: *mut c_void = &mut null_replace_value as *mut f32 as *mut c_void; fitsio_sys::ffgcv( - metafits_fptr.as_raw(), + fits_fptr.fits_file.as_raw(), 82, col_num, row + 1, @@ -968,8 +967,8 @@ pub fn read_cell_array_f64( 0 => { trace!( "read_cell_array_f64() filename: '{}' hdu: {} col_name: '{}' row '{}'", - metafits_fptr.filename.display(), - metafits_table_hdu.number, + fits_fptr.filename.display(), + fits_table_hdu.number, col_name, row ); @@ -981,15 +980,15 @@ pub fn read_cell_array_f64( println!( "ERROR {} read_cell_array_f64() filename: '{}' hdu: {} col_name: '{}' row '{}'", status, - metafits_fptr.filename.display(), - metafits_table_hdu.number, + fits_fptr.filename.display(), + fits_table_hdu.number, col_name, row ); Err(FitsError::CellArray { - fits_filename: metafits_fptr.filename.clone(), - hdu_num: metafits_table_hdu.number + 1, + fits_filename: fits_fptr.filename.clone(), + hdu_num: fits_table_hdu.number + 1, row_num: row, col_name: col_name.to_string(), }) diff --git a/src/fits_read/test.rs b/src/fits_read/test.rs index 8052349..a174cdb 100644 --- a/src/fits_read/test.rs +++ b/src/fits_read/test.rs @@ -26,7 +26,8 @@ fn test_get_hdu_image_size_image() { }; // Create a new image HDU - fptr.create_image("EXTNAME".to_string(), &image_description) + fptr.fits_file + .create_image("EXTNAME".to_string(), &image_description) .unwrap(); let hdu = fits_open_hdu!(fptr, 1).expect("Couldn't open HDU 1"); @@ -56,7 +57,8 @@ fn test_get_hdu_image_size_non_image() { .unwrap(); let descriptions = [first_description, second_description]; - fptr.create_table("EXTNAME".to_string(), &descriptions) + fptr.fits_file + .create_table("EXTNAME".to_string(), &descriptions) .unwrap(); let hdu = fits_open_hdu!(fptr, 1).expect("Couldn't open HDU 1"); @@ -78,12 +80,15 @@ fn test_get_fits_image_valid_f32() { }; // Create a new image HDU - fptr.create_image("EXTNAME".to_string(), &image_description) + fptr.fits_file + .create_image("EXTNAME".to_string(), &image_description) .unwrap(); let hdu = fits_open_hdu!(fptr, 1).expect("Couldn't open HDU 1"); // Write some data - assert!(hdu.write_image(fptr, &[1.0, 2.0, 3.0]).is_ok()); + assert!(hdu + .write_image(&mut fptr.fits_file, &[1.0, 2.0, 3.0]) + .is_ok()); // Run our test, check dimensions let size_vec = get_hdu_image_size!(fptr, &hdu).unwrap(); @@ -112,12 +117,13 @@ fn test_get_fits_image_valid_i32() { }; // Create a new image HDU - fptr.create_image("EXTNAME".to_string(), &image_description) + fptr.fits_file + .create_image("EXTNAME".to_string(), &image_description) .unwrap(); let hdu = fits_open_hdu!(fptr, 1).expect("Couldn't open HDU 1"); // Write some data - assert!(hdu.write_image(fptr, &[-1, 0, 1]).is_ok()); + assert!(hdu.write_image(&mut fptr.fits_file, &[-1, 0, 1]).is_ok()); // Run our test, check dimensions let size_vec = get_hdu_image_size!(fptr, &hdu).unwrap(); @@ -146,12 +152,15 @@ fn test_get_fits_image_invalid() { }; // Create a new image HDU - fptr.create_image("EXTNAME".to_string(), &image_description) + fptr.fits_file + .create_image("EXTNAME".to_string(), &image_description) .unwrap(); let hdu = fits_open_hdu!(fptr, 1).expect("Couldn't open HDU 1"); // Write some data - assert!(hdu.write_image(fptr, &[-12345678, 0, 12345678]).is_ok()); + assert!(hdu + .write_image(&mut fptr.fits_file, &[-12345678, 0, 12345678]) + .is_ok()); // Run our test, check dimensions let size_vec = get_hdu_image_size!(fptr, &hdu).unwrap(); @@ -194,7 +203,8 @@ fn test_get_fits_image_not_image() { .unwrap(); let descriptions = [first_description, second_description]; - fptr.create_table("EXTNAME".to_string(), &descriptions) + fptr.fits_file + .create_table("EXTNAME".to_string(), &descriptions) .unwrap(); let hdu = fits_open_hdu!(fptr, 1).expect("Couldn't open HDU 1"); @@ -225,22 +235,22 @@ fn test_get_required_fits_key() { assert!(doesnt_exist.is_err()); // Key types must be i64 to get any sort of sanity. - hdu.write_key(fptr, "foo", 10i64) + hdu.write_key(&mut fptr.fits_file, "foo", 10i64) .expect("Couldn't write key 'foo'"); - hdu.write_key(fptr, "bar", -5i64) + hdu.write_key(&mut fptr.fits_file, "bar", -5i64) .expect("Couldn't write key 'bar'"); // Verify that using the normal `fitsio` gives the wrong result, unless // the type is an i64. - let fits_value_i32 = hdu.read_key::(fptr, "FOO"); - let fits_value_i64 = hdu.read_key::(fptr, "FOO"); + let fits_value_i32 = hdu.read_key::(&mut fptr.fits_file, "FOO"); + let fits_value_i64 = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fits_value_i32.is_ok()); assert!(fits_value_i64.is_ok()); assert_eq!(fits_value_i32.unwrap(), 1); assert_eq!(fits_value_i64.unwrap(), 10); // Despite writing to "fits_value", the key is written as "FOO". - let fits_value_i64 = hdu.read_key::(fptr, "FOO"); + let fits_value_i64 = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fits_value_i64.is_ok()); assert_eq!(fits_value_i64.unwrap(), 10); @@ -264,7 +274,7 @@ fn test_get_required_fits_key() { fn test_get_optional_fits_key() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("test_fits_read_key.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // Failure to get a key that doesn't exist is OK if we're using the optional variant. let fits_value: Result, _> = get_optional_fits_key!(fptr, &hdu, "foo"); @@ -272,22 +282,22 @@ fn test_get_optional_fits_key() { assert!(fits_value.unwrap().is_none()); // Key types must be i64 to get any sort of sanity. - hdu.write_key(fptr, "foo", 10i64) + hdu.write_key(&mut fptr.fits_file, "foo", 10i64) .expect("Couldn't write key 'foo'"); - hdu.write_key(fptr, "bar", -5i64) + hdu.write_key(&mut fptr.fits_file, "bar", -5i64) .expect("Couldn't write key 'bar'"); // Verify that using the normal `fitsio` gives the wrong result, unless // the type is an i64. - let fits_value_i32 = hdu.read_key::(fptr, "FOO"); - let fits_value_i64 = hdu.read_key::(fptr, "FOO"); + let fits_value_i32 = hdu.read_key::(&mut fptr.fits_file, "FOO"); + let fits_value_i64 = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fits_value_i32.is_ok()); assert!(fits_value_i64.is_ok()); assert_eq!(fits_value_i32.unwrap(), 1); assert_eq!(fits_value_i64.unwrap(), 10); // Despite writing to "foo", the key is written as "FOO". - let fits_value_i64 = hdu.read_key::(fptr, "FOO"); + let fits_value_i64 = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fits_value_i64.is_ok()); assert_eq!(fits_value_i64.unwrap(), 10); @@ -319,7 +329,7 @@ fn test_get_required_fits_key_string() { assert!(does_not_exist.is_err()); // Add a test string - hdu.write_key(fptr, "fits_value", "hello") + hdu.write_key(&mut fptr.fits_file, "fits_value", "hello") .expect("Couldn't write key 'fits_value'"); // Read fits_value back in @@ -334,7 +344,7 @@ fn test_get_required_fits_key_string() { fn test_get_optional_fits_key_string() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("test_fits_read_key_string.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // No Failure to get a key that doesn't exist. let does_not_exist: Result, FitsError> = @@ -344,7 +354,7 @@ fn test_get_optional_fits_key_string() { assert!(does_not_exist.unwrap().is_none()); // Add a test string - hdu.write_key(fptr, "fits_value", "hello") + hdu.write_key(&mut fptr.fits_file, "fits_value", "hello") .expect("Couldn't write key 'fits_value'"); // Read fits_value back in @@ -367,7 +377,7 @@ fn test_get_fits_long_string() { // Sadly, rust's `fitsio` library doesn't support writing long strings // with CONTINUE statements. We have to do it for ourselves. unsafe { - let fptr_ffi = fptr.as_raw(); + let fptr_ffi = fptr.fits_file.as_raw(); let keyword_ffi = CString::new("foo").expect("get_fits_long_string: CString::new() failed for 'foo'"); let value_ffi = CString::new(complete_string) @@ -383,19 +393,19 @@ fn test_get_fits_long_string() { ); } - let hdu = fptr.hdu(0).unwrap(); + let hdu = fptr.fits_file.hdu(0).unwrap(); let fits_value_str = get_required_fits_key_long_string!(fptr, &hdu, "FOO"); assert!(fits_value_str.is_ok()); assert_eq!(fits_value_str.unwrap(), complete_string); // Try out the `fitsio` read key. - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); - let fitsio_str = hdu.read_key::(fptr, "FOO"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); + let fitsio_str = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fitsio_str.is_ok()); assert_eq!(fitsio_str.unwrap(), first_string); // A repeated read just returns the first string again. - let fitsio_str = hdu.read_key::(fptr, "FOO"); + let fitsio_str = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fitsio_str.is_ok()); assert_eq!(fitsio_str.unwrap(), first_string); }); @@ -411,7 +421,7 @@ fn test_get_required_fits_long_string() { // Sadly, rust's `fitsio` library doesn't support writing long strings // with CONTINUE statements. We have to do it for ourselves. unsafe { - let fptr_ffi = fptr.as_raw(); + let fptr_ffi = fptr.fits_file.as_raw(); let keyword_ffi = CString::new("foo").expect("get_fits_long_string: CString::new() failed for 'foo'"); let value_ffi = CString::new(complete_string) @@ -427,7 +437,7 @@ fn test_get_required_fits_long_string() { ); } - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // Check for a valid long string let result1 = get_required_fits_key_long_string!(fptr, &hdu, "FOO"); @@ -435,13 +445,13 @@ fn test_get_required_fits_long_string() { assert_eq!(result1.unwrap(), complete_string); // Try out the `fitsio` read key. - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); - let fitsio_str = hdu.read_key::(fptr, "FOO"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); + let fitsio_str = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fitsio_str.is_ok()); assert_eq!(fitsio_str.unwrap(), first_string); // A repeated read just returns the first string again. - let fitsio_str = hdu.read_key::(fptr, "FOO"); + let fitsio_str = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fitsio_str.is_ok()); assert_eq!(fitsio_str.unwrap(), first_string); @@ -470,7 +480,7 @@ fn test_get_optional_fits_long_string() { // Sadly, rust's `fitsio` library doesn't support writing long strings // with CONTINUE statements. We have to do it for ourselves. unsafe { - let fptr_ffi = fptr.as_raw(); + let fptr_ffi = fptr.fits_file.as_raw(); let keyword_ffi = CString::new("foo").expect("get_fits_long_string: CString::new() failed for 'foo'"); let value_ffi = CString::new(complete_string) @@ -486,7 +496,7 @@ fn test_get_optional_fits_long_string() { ); } - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // Read a key that IS there let result1 = get_optional_fits_key_long_string!(fptr, &hdu, "FOO"); @@ -497,12 +507,12 @@ fn test_get_optional_fits_long_string() { assert_eq!(fits_value1_str.unwrap(), complete_string); // Try out the `fitsio` read key. - let fitsio_str = hdu.read_key::(fptr, "FOO"); + let fitsio_str = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fitsio_str.is_ok()); assert_eq!(fitsio_str.unwrap(), first_string); // A repeated read just returns the first string again. - let fitsio_str = hdu.read_key::(fptr, "FOO"); + let fitsio_str = hdu.read_key::(&mut fptr.fits_file, "FOO"); assert!(fitsio_str.is_ok()); assert_eq!(fitsio_str.unwrap(), first_string); @@ -525,7 +535,7 @@ fn test_get_fits_long_string_failure() { // Sadly, rust's `fitsio` library doesn't support writing long strings // with CONTINUE statements. We have to do it for ourselves. unsafe { - let fptr_ffi = fptr.as_raw(); + let fptr_ffi = fptr.fits_file.as_raw(); let keyword_ffi = CString::new("fits_value") .expect("get_fits_long_string: CString::new() failed for 'fits_value'"); let value_ffi = CString::new(complete_string) @@ -541,7 +551,7 @@ fn test_get_fits_long_string_failure() { ); } - let hdu = fptr.hdu(0).unwrap(); + let hdu = fptr.fits_file.hdu(0).unwrap(); let fits_value_str = get_required_fits_key_long_string!(fptr, &hdu, "NOTfits_value"); assert!(fits_value_str.is_err()); }); diff --git a/src/gpubox_files/mod.rs b/src/gpubox_files/mod.rs index 1dc12d3..e513ca5 100644 --- a/src/gpubox_files/mod.rs +++ b/src/gpubox_files/mod.rs @@ -11,13 +11,16 @@ use std::collections::HashSet; use std::fmt; use std::path::Path; -use fitsio::{hdu::FitsHdu, FitsFile}; +use fitsio::hdu::FitsHdu; use rayon::prelude::*; use regex::Regex; use crate::*; pub use error::GpuboxError; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; + #[cfg(test)] mod test; @@ -32,11 +35,15 @@ 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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass)] pub struct GpuBoxBatch { - pub batch_number: usize, // 00,01,02..n - pub gpubox_files: Vec, // Vector storing the details of each gpubox file in this batch + /// Batch number: 00,01,02..n. + pub batch_number: usize, + + /// Vector storing the details of each gpubox file in this batch + pub gpubox_files: Vec, } impl GpuBoxBatch { @@ -59,10 +66,13 @@ 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))] #[derive(Clone)] pub struct GpuBoxFile { /// Filename of gpubox file pub filename: String, + /// channel number (Legacy==gpubox host number 01..24; V2==receiver channel number 001..255) pub channel_identifier: usize, } @@ -248,7 +258,7 @@ pub(crate) fn examine_gpubox_files>( // Check that there are some HDUs (apart from just the primary) // Assuming it does have some, open the first one - let hdu = match fptr.iter().count() { + let hdu = match fptr.fits_file.iter().count() { 1 => { return Err(GpuboxError::NoDataHDUsInGpuboxFile { gpubox_filename: g.filename.clone(), @@ -408,7 +418,7 @@ fn determine_gpubox_batches>( /// /// # Arguments /// -/// * `gpubox_fptr` - A FitsFile reference to this gpubox file. +/// * `gpubox_fptr` - An MWAFitsFile reference to this gpubox file. /// /// * `gpubox_hdu_fptr` - A reference to the HDU we are finding the time of. /// @@ -419,7 +429,7 @@ fn determine_gpubox_batches>( /// /// fn determine_hdu_time( - gpubox_fptr: &mut FitsFile, + gpubox_fptr: &mut MWAFitsFile, gpubox_hdu_fptr: &FitsHdu, ) -> Result { let start_unix_time: u64 = get_required_fits_key!(gpubox_fptr, gpubox_hdu_fptr, "TIME")?; @@ -434,7 +444,7 @@ fn determine_hdu_time( /// /// # Arguments /// -/// * `gpubox_fptr` - A FitsFile reference to this gpubox file. +/// * `gpubox_fptr` - An MWAFitsFile reference to this gpubox file. /// /// * `mwa_version` - enum telling us which correlator version the observation was created by. /// @@ -445,11 +455,11 @@ fn determine_hdu_time( /// /// fn map_unix_times_to_hdus( - gpubox_fptr: &mut FitsFile, + gpubox_fptr: &mut MWAFitsFile, mwa_version: MWAVersion, ) -> Result, FitsError> { let mut map = BTreeMap::new(); - let last_hdu_index = gpubox_fptr.iter().count(); + let last_hdu_index = gpubox_fptr.fits_file.iter().count(); // The new correlator has a "weights" HDU in each alternating HDU. Skip // those. let step_size = if mwa_version == MWAVersion::CorrMWAXv2 { @@ -474,7 +484,7 @@ fn map_unix_times_to_hdus( /// /// # Arguments /// -/// * `gpubox_fptr` - A FitsFile reference to this gpubox file. +/// * `gpubox_fptr` - An MWAFitsFile reference to this gpubox file. /// /// * `gpubox_primary_hdu` - The primary HDU of the gpubox file. /// @@ -489,7 +499,7 @@ fn map_unix_times_to_hdus( /// /// fn validate_gpubox_metadata_mwa_version( - gpubox_fptr: &mut FitsFile, + gpubox_fptr: &mut MWAFitsFile, gpubox_primary_hdu: &FitsHdu, gpubox_filename: &str, mwa_version: MWAVersion, @@ -527,7 +537,7 @@ fn validate_gpubox_metadata_mwa_version( /// /// # Arguments /// -/// * `gpubox_fptr` - A FitsFile reference to this gpubox file. +/// * `gpubox_fptr` - An MWAFitsFile reference to this gpubox file. /// /// * `gpubox_primary_hdu` - The primary HDU of the gpubox file. /// @@ -542,7 +552,7 @@ fn validate_gpubox_metadata_mwa_version( /// /// fn validate_gpubox_metadata_obs_id( - gpubox_fptr: &mut FitsFile, + gpubox_fptr: &mut MWAFitsFile, gpubox_primary_hdu: &FitsHdu, gpubox_filename: &str, metafits_obs_id: u32, @@ -610,7 +620,7 @@ fn create_time_map( } } - // Get the UNIX times from each of the HDUs of this `FitsFile`. + // Get the UNIX times from each of the HDUs of this `MWAFitsFile`. map_unix_times_to_hdus(&mut fptr, mwa_version).map_err(GpuboxError::from) }) .collect::, GpuboxError>>>(); diff --git a/src/gpubox_files/test.rs b/src/gpubox_files/test.rs index 04175c4..559fae3 100644 --- a/src/gpubox_files/test.rs +++ b/src/gpubox_files/test.rs @@ -371,13 +371,13 @@ fn test_no_hdus() { fn test_determine_hdu_time_test1() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("determine_hdu_time_test1.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // Write the TIME and MILLITIM keys. Key types must be i64 to get any // sort of sanity. - hdu.write_key(fptr, "TIME", 1_434_494_061) + hdu.write_key(&mut fptr.fits_file, "TIME", 1_434_494_061) .expect("Couldn't write key 'TIME'"); - hdu.write_key(fptr, "MILLITIM", 0) + hdu.write_key(&mut fptr.fits_file, "MILLITIM", 0) .expect("Couldn't write key 'MILLITIM'"); let result = determine_hdu_time(fptr, &hdu); @@ -390,11 +390,11 @@ fn test_determine_hdu_time_test1() { fn test_determine_hdu_time_test2() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("determine_hdu_time_test2.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); - hdu.write_key(fptr, "TIME", 1_381_844_923) + hdu.write_key(&mut fptr.fits_file, "TIME", 1_381_844_923) .expect("Couldn't write key 'TIME'"); - hdu.write_key(fptr, "MILLITIM", 500) + hdu.write_key(&mut fptr.fits_file, "MILLITIM", 500) .expect("Couldn't write key 'MILLITIM'"); let result = determine_hdu_time(fptr, &hdu); @@ -413,11 +413,11 @@ fn test_determine_hdu_time_test3() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("determine_hdu_time_test3.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); - hdu.write_key(fptr, "TIME", current) + hdu.write_key(&mut fptr.fits_file, "TIME", current) .expect("Couldn't write key 'TIME'"); - hdu.write_key(fptr, "MILLITIM", 500) + hdu.write_key(&mut fptr.fits_file, "MILLITIM", 500) .expect("Couldn't write key 'MILLITIM'"); let result = determine_hdu_time(fptr, &hdu); @@ -439,11 +439,12 @@ fn test_map_unix_times_to_hdus_test() { }; for (i, (time, millitime)) in times.iter().enumerate() { let hdu = fptr + .fits_file .create_image("EXTNAME".to_string(), &image_description) .expect("Couldn't create image"); - hdu.write_key(fptr, "TIME", *time) + hdu.write_key(&mut fptr.fits_file, "TIME", *time) .expect("Couldn't write key 'TIME'"); - hdu.write_key(fptr, "MILLITIM", *millitime) + hdu.write_key(&mut fptr.fits_file, "MILLITIM", *millitime) .expect("Couldn't write key 'MILLITIM'"); expected.insert(time * 1000 + millitime, i + 1); @@ -552,7 +553,7 @@ fn test_determine_common_times_test_one_timestep() { fn test_validate_gpubox_metadata_mwa_version() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("test_validate_gpubox_metadata_mwa_version.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // This should succeed- LegacyOld should NOT have CORR_VER key assert!(validate_gpubox_metadata_mwa_version( @@ -582,7 +583,7 @@ fn test_validate_gpubox_metadata_mwa_version() { .is_err()); // Now put in a corr version - hdu.write_key(fptr, "CORR_VER", 2) + hdu.write_key(&mut fptr.fits_file, "CORR_VER", 2) .expect("Couldn't write key 'CORR_VER'"); // This should succeed- V2 should have CORR_VER key @@ -616,12 +617,12 @@ fn test_validate_gpubox_metadata_mwa_version() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope // This section tests CORR_VER where it is != 2 with_new_temp_fits_file("test_validate_gpubox_metadata_mwa_version.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // This should not succeed- CORR_VER key if it exists should be 2 // CORR_VER did not exist in OldLegacy or Legacy correlator // Now put in a corr version - hdu.write_key(fptr, "CORR_VER", 1) + hdu.write_key(&mut fptr.fits_file, "CORR_VER", 1) .expect("Couldn't write key 'CORR_VER'"); assert!(validate_gpubox_metadata_mwa_version( @@ -638,7 +639,7 @@ fn test_validate_gpubox_metadata_mwa_version() { fn test_validate_gpubox_metadata_obsid() { // with_temp_file creates a temp dir and temp file, then removes them once out of scope with_new_temp_fits_file("test_validate_gpubox_metadata_mwa_version.fits", |fptr| { - let hdu = fptr.hdu(0).expect("Couldn't open HDU 0"); + let hdu = fptr.fits_file.hdu(0).expect("Couldn't open HDU 0"); // OBSID is not there, this should be an error assert!(validate_gpubox_metadata_obs_id( @@ -650,7 +651,7 @@ fn test_validate_gpubox_metadata_obsid() { .is_err()); // Now add the key - hdu.write_key(fptr, "OBSID", 1_234_567_890) + hdu.write_key(&mut fptr.fits_file, "OBSID", 1_234_567_890) .expect("Couldn't write key 'OBSID'"); // OBSID is there, but does not match metafits- this should be an error diff --git a/src/lib.rs b/src/lib.rs index eaa87c6..4bebbb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,9 @@ mod fits_read; mod gpubox_files; mod metafits_context; mod misc; +mod mwa_fits_file; mod rfinput; +mod signal_chain_correction; mod timestep; mod voltage_context; mod voltage_files; @@ -68,6 +70,7 @@ pub use metafits_context::{ }; pub use misc::*; pub use rfinput::{error::RfinputError, Pol, ReceiverType, Rfinput}; +pub use signal_chain_correction::*; pub use timestep::TimeStep; pub use voltage_context::VoltageContext; pub use voltage_files::error::VoltageFileError; @@ -77,4 +80,4 @@ pub use fitsio; pub use fitsio_sys; #[cfg(feature = "python")] -mod python; +pub mod python; diff --git a/src/metafits_context/mod.rs b/src/metafits_context/mod.rs index fce15ca..7ce8487 100644 --- a/src/metafits_context/mod.rs +++ b/src/metafits_context/mod.rs @@ -12,6 +12,7 @@ use fitsio::hdu::HduInfo; use num_derive::FromPrimitive; use num_traits::ToPrimitive; +use self::error::MetafitsError; use crate::antenna::*; use crate::baseline::*; use crate::coarse_channel::*; @@ -20,12 +21,15 @@ use crate::rfinput::*; use crate::voltage_files::*; use crate::*; -use self::error::MetafitsError; pub mod error; #[cfg(test)] mod test; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass_enum; #[cfg(feature = "python")] mod python; @@ -33,7 +37,7 @@ mod python; /// #[repr(C)] #[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] pub enum MWAVersion { /// MWA correlator (v1.0), having data files without any batch numbers. CorrOldLegacy = 1, @@ -78,7 +82,8 @@ impl fmt::Display for MWAVersion { /// Visibility polarisations /// #[repr(C)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] pub enum VisPol { XX = 1, XY = 2, @@ -116,7 +121,7 @@ impl fmt::Display for VisPol { /// #[repr(C)] #[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)] -#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] pub enum GeometricDelaysApplied { No = 0, Zenith = 1, @@ -181,7 +186,7 @@ impl std::str::FromStr for GeometricDelaysApplied { /// #[repr(C)] #[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)] -#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] pub enum CableDelaysApplied { NoCableDelaysApplied = 0, CableAndRecClock = 1, @@ -234,7 +239,7 @@ impl std::str::FromStr for CableDelaysApplied { #[repr(C)] #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[allow(non_camel_case_types, clippy::upper_case_acronyms)] -#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] pub enum MWAMode { No_Capture = 0, Burst_Vsib = 1, @@ -337,60 +342,12 @@ impl std::str::FromStr for MWAMode { } } -/// -/// Signal chain correction table -/// -#[derive(Clone, Debug, PartialEq)] -#[repr(C)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] -pub struct SignalChainCorrection { - /// Receiver Type - pub receiver_type: ReceiverType, - - /// Whitening Filter - pub whitening_filter: bool, - - /// Corrections - pub corrections: Vec, -} - -/// Implements fmt::Display for SignalChainCorrection -/// -/// # Arguments -/// -/// * `f` - A fmt::Formatter -/// -/// -/// # Returns -/// -/// * `fmt::Result` - Result of this method -/// -/// -impl fmt::Display for SignalChainCorrection { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let corr: String = if !self.corrections.is_empty() { - format!( - "[{}..{}]", - self.corrections[0], - self.corrections[MAX_RECEIVER_CHANNELS - 1] - ) - } else { - "[]".to_string() - }; - - write!( - f, - "Receiver Type: {} Whitening filter: {} Corrections: {}", - self.receiver_type, self.whitening_filter, corr - ) - } -} - /// /// 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))] #[derive(Clone, Debug)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct MetafitsContext { /// mwa version pub mwa_version: Option, @@ -406,7 +363,7 @@ pub struct MetafitsContext { pub sched_end_unix_time_ms: u64, /// Scheduled start (UTC) of observation pub sched_start_utc: DateTime, - /// Scheduled end (UTC) of observation + /// Scheduled end (UTC) of observation pub sched_end_utc: DateTime, /// Scheduled start (MJD) of observation pub sched_start_mjd: f64, @@ -533,7 +490,7 @@ pub struct MetafitsContext { pub num_baselines: usize, /// Baslines pub baselines: Vec, - /// Number of polarisation combinations in the visibilities e.g. XX,XY,YX,YY == 4 + /// Number of polarisation combinations in the visibilities e.g. XX,XY,YX,YY == 4 pub num_visibility_pols: usize, /// Filename of the metafits we were given pub metafits_filename: String, @@ -576,7 +533,7 @@ impl MetafitsContext { /// # Returns /// /// * Result containing a populated MetafitsContext object if Ok. - /// + /// pub fn new>( metafits: P, mwa_version: Option, @@ -1126,7 +1083,7 @@ impl MetafitsContext { /// /// # Arguments /// - /// * `metafits_fptr` - reference to the FitsFile representing the metafits file. + /// * `metafits_fptr` - reference to the MWAFitsFile representing the metafits file. /// /// * `sig_chain_hdu` - The FitsHdu containing valid signal chain corrections data. /// @@ -1135,7 +1092,7 @@ impl MetafitsContext { /// * Result containing a vector of signal chain corrections read from the sig_chain_hdu HDU. /// fn populate_signal_chain_corrections( - metafits_fptr: &mut fitsio::FitsFile, + metafits_fptr: &mut MWAFitsFile, sig_chain_hdu: &fitsio::hdu::FitsHdu, ) -> Result, FitsError> { // Find out how many rows there are in the table diff --git a/src/metafits_context/python.rs b/src/metafits_context/python.rs index af8198c..2305202 100644 --- a/src/metafits_context/python.rs +++ b/src/metafits_context/python.rs @@ -7,29 +7,25 @@ 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)] +#[cfg_attr(feature = "python", pymethods)] #[cfg(feature = "python")] -#[pymethods] impl MetafitsContext { + #[new] + #[pyo3(signature = (metafits_filename, mwa_version=None), text_signature = "(metafits_filename: str, mwa_version: typing.Optional[MWAVersion]=None)")] /// From a path to a metafits file, create a `MetafitsContext`. /// - /// # Arguments - /// - /// * `metafits_filename` - filename of metafits file as a path or string. - /// - /// * `mwa_version` - (Optional) the MWA version the metafits should be interpreted as. Pass None to have mwalib guess based on the MODE in the metafits. + /// Args: + /// metafits_filename (str): filename of metafits file. + /// mwa_version (Optional[MWAVersion]): the MWA version the metafits should be interpreted as. Pass None to have mwalib guess based on the MODE in the metafits. /// - /// # Returns - /// - /// * A populated MetafitsContext object if Ok. - /// - #[new] - #[pyo3(signature = (metafits_filename, mwa_version=None))] - fn pyo3_new( - metafits_filename: pyo3::PyObject, - mwa_version: Option, - ) -> pyo3::PyResult { - let m = Self::new(metafits_filename.to_string(), mwa_version)?; + /// Returns: + /// metafits_contex (MetafitsContex): a populated MetafitsContext object if Ok. + fn pyo3_new(metafits_filename: &str, mwa_version: Option) -> pyo3::PyResult { + let m = Self::new(metafits_filename, mwa_version)?; Ok(m) } diff --git a/src/misc/test.rs b/src/misc/test.rs index b6c2891..c2073ed 100644 --- a/src/misc/test.rs +++ b/src/misc/test.rs @@ -9,6 +9,7 @@ use core::f32; #[cfg(test)] use super::*; use crate::antenna::*; +use crate::fits_read::*; use crate::rfinput::*; use float_cmp::*; @@ -27,7 +28,7 @@ use float_cmp::*; #[cfg(test)] pub fn with_new_temp_fits_file(filename: &str, callback: F) where - F: for<'a> Fn(&'a mut fitsio::FitsFile), + F: for<'a> Fn(&'a mut MWAFitsFile), { let tdir = tempdir::TempDir::new("fitsio-").unwrap(); let tdir_path = tdir.path(); @@ -35,11 +36,13 @@ where let filename_str = filename.to_str().expect("cannot create string filename"); - let mut fptr = fitsio::FitsFile::create(filename_str) + let fptr = fitsio::FitsFile::create(filename_str) .open() .expect("Couldn't open tempfile"); - callback(&mut fptr); + let mut mwa_fits_file = MWAFitsFile::new(filename_str.into(), fptr); + + callback(&mut mwa_fits_file); } #[test] diff --git a/src/mwa_fits_file/mod.rs b/src/mwa_fits_file/mod.rs new file mode 100644 index 0000000..0e597fc --- /dev/null +++ b/src/mwa_fits_file/mod.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Encapsulating filename and FitsFile + +// This struct encapsulates the fitsio::FitsFile struct and adds back +// the 'filename' property that was removed in v0.21.0 of the fitsio crate +// +use fitsio::FitsFile; +use std::path::PathBuf; + +#[cfg(test)] +mod test; + +pub struct MWAFitsFile { + /// FitsFile struct + pub fits_file: FitsFile, + + /// Filename (filename, including + /// the full or relative path to the file) + pub filename: PathBuf, +} + +impl MWAFitsFile { + /// Creates an encapsulating struct to hold a FitsFile object and + /// it's filename. Filename was removed from fitsio in 0.21, so + /// this is a way to retain that property since we use it in error + /// messages and debug. + /// + /// # Arguments + /// + /// * `filename` - filename of FITS file as a path or string. + /// + /// * `fits_file` - an already created fitsio::FitsFile struct + /// + /// # Returns + /// + /// * A populated MWAFits object + /// + pub fn new(filename: PathBuf, fits_file: FitsFile) -> Self { + MWAFitsFile { + fits_file, + filename, + } + } +} diff --git a/src/mwa_fits_file/test.rs b/src/mwa_fits_file/test.rs new file mode 100644 index 0000000..fd6a42c --- /dev/null +++ b/src/mwa_fits_file/test.rs @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Unit tests for the mwa_fits_file module +#[cfg(test)] +use fitsio::FitsFile; + +#[test] +fn test_mwa_fits_file_new() { + use super::MWAFitsFile; + + let metafits_filename = "test_files/1101503312_1_timestep/1101503312.metafits"; + + let fptr = FitsFile::open(metafits_filename).expect("Could not open fits file!"); + + let mut mwa_fits_file = MWAFitsFile::new(metafits_filename.into(), fptr); + + // Check it works! + assert_eq!( + mwa_fits_file.filename.display().to_string(), + metafits_filename + ); + + // Open a hdu + let hdu = mwa_fits_file + .fits_file + .hdu(0) + .expect("Could not open PRIMARY HDU"); + + // Read the obs_id from the fits file + let obs_id = hdu + .read_key::(&mut mwa_fits_file.fits_file, "GPSTIME") + .expect("Cannot read key"); + + // Ensure it is what is expected + assert_eq!(obs_id, 1101503312); +} diff --git a/src/python.rs b/src/python.rs index d0008d8..e3dab67 100644 --- a/src/python.rs +++ b/src/python.rs @@ -3,30 +3,88 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! Python interface to mwalib via pyo3. - +#[cfg(feature = "python")] +use pyo3::exceptions::PyException; +#[cfg(feature = "python")] use pyo3::prelude::*; +#[cfg(feature = "python")] +use pyo3_stub_gen::{create_exception, define_stub_info_gatherer}; use crate::{ gpubox_files::error::*, rfinput::{Pol, ReceiverType}, voltage_files::error::*, - CableDelaysApplied, CorrelatorContext, GeometricDelaysApplied, MWAMode, MWAVersion, - MetafitsContext, VoltageContext, + Antenna, CableDelaysApplied, CorrelatorContext, GeometricDelaysApplied, MWAMode, MWAVersion, + MetafitsContext, Rfinput, SignalChainCorrection, VoltageContext, }; -// Add a python exception for mwalib. -pyo3::create_exception!(mwalib, MwalibError, pyo3::exceptions::PyException); +// Add a python exception for MmwalibError. +create_exception!(mwalib, MwalibError, PyException); impl std::convert::From for PyErr { fn from(err: crate::MwalibError) -> PyErr { MwalibError::new_err(err.to_string()) } } +// Other exceptions +create_exception!(mwalib, GpuboxErrorBatchMissing, PyException); +create_exception!(mwalib, GpuboxErrorCorrVerMismatch, PyException); +create_exception!(mwalib, GpuboxErrorEmptyBTreeMap, PyException); +create_exception!(mwalib, GpuboxErrorFits, PyException); +create_exception!(mwalib, GpuboxErrorInvalidCoarseChanIndex, PyException); +create_exception!(mwalib, GpuboxErrorInvalidMwaVersion, PyException); +create_exception!(mwalib, GpuboxErrorInvalidTimeStepIndex, PyException); +create_exception!(mwalib, GpuboxErrorLegacyNaxis1Mismatch, PyException); +create_exception!(mwalib, GpuboxErrorLegacyNaxis2Mismatch, PyException); +create_exception!(mwalib, GpuboxErrorMissingObsid, PyException); +create_exception!(mwalib, GpuboxErrorMixture, PyException); +create_exception!(mwalib, GpuboxErrorMwaxNaxis1Mismatch, PyException); +create_exception!(mwalib, GpuboxErrorMwaxNaxis2Mismatch, PyException); +create_exception!(mwalib, GpuboxErrorMwaxCorrVerMismatch, PyException); +create_exception!(mwalib, GpuboxErrorMwaxCorrVerMissing, PyException); +create_exception!( + mwalib, + GpuboxErrorNoDataForTimeStepCoarseChannel, + PyException +); +create_exception!(mwalib, GpuboxErrorNoDataHDUsInGpuboxFile, PyException); +create_exception!(mwalib, GpuboxErrorNoGpuboxes, PyException); +create_exception!(mwalib, GpuboxErrorObsidMismatch, PyException); +create_exception!(mwalib, GpuboxErrorUnequalHduSizes, PyException); +create_exception!(mwalib, GpuboxErrorUnevenCountInBatches, PyException); +create_exception!(mwalib, GpuboxErrorUnrecognised, PyException); +create_exception!(mwalib, VoltageErrorInvalidTimeStepIndex, PyException); +create_exception!(mwalib, VoltageErrorInvalidCoarseChanIndex, PyException); +create_exception!(mwalib, VoltageErrorNoVoltageFiles, PyException); +create_exception!(mwalib, VoltageErrorInvalidBufferSize, PyException); +create_exception!(mwalib, VoltageErrorInvalidGpsSecondStart, PyException); +create_exception!(mwalib, VoltageErrorInvalidVoltageFileSize, PyException); +create_exception!(mwalib, VoltageErrorInvalidGpsSecondCount, PyException); +create_exception!(mwalib, VoltageErro, PyException); +create_exception!(mwalib, VoltageErrorMixture, PyException); +create_exception!(mwalib, VoltageErrorGpsTimeMissing, PyException); +create_exception!(mwalib, VoltageErrorUnevenChannelsForGpsTime, PyException); +create_exception!(mwalib, VoltageErrorUnrecognised, PyException); +create_exception!(mwalib, VoltageErrorMissingObsid, PyException); +create_exception!(mwalib, VoltageErrorUnequalFileSizes, PyException); +create_exception!(mwalib, VoltageErrorMetafitsObsidMismatch, PyException); +create_exception!(mwalib, VoltageErrorObsidMismatch, PyException); +create_exception!(mwalib, VoltageErrorEmptyBTreeMap, PyException); +create_exception!(mwalib, VoltageErrorInvalidMwaVersion, PyException); +create_exception!( + mwalib, + VoltageErrorNoDataForTimeStepCoarseChannel, + PyException +); + #[cfg_attr(feature = "python", pymodule)] fn mwalib(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -196,3 +254,5 @@ fn mwalib(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("__version__", env!("CARGO_PKG_VERSION"))?; Ok(()) } + +define_stub_info_gatherer!(stub_info); diff --git a/src/rfinput/mod.rs b/src/rfinput/mod.rs index 996971c..50b2070 100644 --- a/src/rfinput/mod.rs +++ b/src/rfinput/mod.rs @@ -5,13 +5,18 @@ //! Structs and helper methods for rf_input metadata pub mod error; -use crate::metafits_context::SignalChainCorrection; use crate::misc::{has_whitening_filter, vec_compare_f32, vec_compare_f64}; +use crate::signal_chain_correction::SignalChainCorrection; use crate::{fits_open_hdu_by_name, fits_read::*}; use core::f32; use error::RfinputError; use std::fmt; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass_enum; + #[cfg(test)] mod test; @@ -85,7 +90,7 @@ fn get_electrical_length(metafits_length_string: String, coax_v_factor: f64) -> /// Instrument polarisation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] pub enum Pol { X, Y, @@ -170,7 +175,7 @@ struct RfInputMetafitsTableRow { /// ReceiverType enum. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(C)] -#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] +#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int), gen_stub_pyclass_enum)] #[allow(clippy::upper_case_acronyms)] pub enum ReceiverType { Unknown, @@ -250,8 +255,9 @@ 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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct Rfinput { /// This is the metafits order (0-n inputs) pub input: u32, @@ -301,7 +307,7 @@ pub struct Rfinput { pub rec_slot_number: u32, /// Receiver type pub rec_type: ReceiverType, - /// Flavour + /// Cable Flavour pub flavour: String, /// Has whitening filter (depends on flavour) pub has_whitening_filter: bool, @@ -368,7 +374,7 @@ impl Rfinput { /// /// # Arguments /// - /// * `metafits_fptr` - reference to the FitsFile representing the metafits file. + /// * `metafits_fptr` - reference to the MWAFitsFile representing the metafits file. /// /// * `metafits_tile_table_hdu` - reference to the HDU containing the TILEDATA table. /// @@ -382,7 +388,7 @@ impl Rfinput { /// * An Result containing a populated vector of RFInputMetafitsTableRow structss or an Error /// fn read_metafits_tiledata_values( - metafits_fptr: &mut fitsio::FitsFile, + metafits_fptr: &mut MWAFitsFile, metafits_tile_table_hdu: &fitsio::hdu::FitsHdu, row: usize, num_coarse_chans: usize, @@ -496,7 +502,7 @@ impl Rfinput { /// /// # Arguments /// - /// * `metafits_fptr` - reference to the FitsFile representing the metafits file. + /// * `metafits_fptr` - reference to the MWAFitsFile representing the metafits file. /// /// * `metafits_calibdata_table_hdu` - reference to the HDU containing the CALIBDATA table. /// @@ -510,7 +516,7 @@ impl Rfinput { /// * An Result containing a populated vector of RFInputMetafitsTableRow structss or an Error /// fn read_metafits_calibdata_values( - metafits_fptr: &mut fitsio::FitsFile, + metafits_fptr: &mut MWAFitsFile, metafits_calibdata_table_hdu: &Option, row: usize, num_coarse_chans: usize, @@ -567,7 +573,7 @@ impl Rfinput { /// /// * `num_inputs` - number of rf_inputs to read from the metafits TILEDATA bintable. /// - /// * `metafits_fptr` - reference to the FitsFile representing the metafits file. + /// * `metafits_fptr` - reference to the MWAFitsFile representing the metafits file. /// /// * `metafits_tile_table_hdu` - reference to the HDU containing the TILEDATA table. /// @@ -581,7 +587,7 @@ impl Rfinput { /// pub(crate) fn populate_rf_inputs( num_inputs: usize, - metafits_fptr: &mut fitsio::FitsFile, + metafits_fptr: &mut MWAFitsFile, metafits_tile_table_hdu: fitsio::hdu::FitsHdu, coax_v_factor: f64, num_coarse_chans: usize, diff --git a/src/rfinput/test.rs b/src/rfinput/test.rs index 549f65e..cd1ecaa 100644 --- a/src/rfinput/test.rs +++ b/src/rfinput/test.rs @@ -113,6 +113,7 @@ fn test_read_metafits_tiledata_values_from_invalid_metafits() { let descriptions = [first_description, second_description]; metafits_fptr + .fits_file .create_table("TILEDATA".to_string(), &descriptions) .unwrap(); @@ -177,10 +178,12 @@ fn test_read_metafits_calibdata_values_from_invalid_metafits() { let descriptions = [first_description, second_description]; metafits_fptr + .fits_file .create_table("TILEDATA".to_string(), &descriptions) .unwrap(); metafits_fptr + .fits_file .create_table("CALIBDATA".to_string(), &descriptions) .unwrap(); diff --git a/src/signal_chain_correction/mod.rs b/src/signal_chain_correction/mod.rs new file mode 100644 index 0000000..50eb8cd --- /dev/null +++ b/src/signal_chain_correction/mod.rs @@ -0,0 +1,56 @@ +use crate::rfinput::ReceiverType; +use crate::MAX_RECEIVER_CHANNELS; +use std::fmt; + +#[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))] +#[derive(Clone, Debug, PartialEq)] +#[repr(C)] +pub struct SignalChainCorrection { + /// Receiver Type + pub receiver_type: ReceiverType, + + /// Whitening Filter + pub whitening_filter: bool, + + /// Corrections + pub corrections: Vec, +} + +/// Implements fmt::Display for SignalChainCorrection +/// +/// # Arguments +/// +/// * `f` - A fmt::Formatter +/// +/// +/// # Returns +/// +/// * `fmt::Result` - Result of this method +/// +/// +impl fmt::Display for SignalChainCorrection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let corr: String = if !self.corrections.is_empty() { + format!( + "[{}..{}]", + self.corrections[0], + self.corrections[MAX_RECEIVER_CHANNELS - 1] + ) + } else { + "[]".to_string() + }; + + write!( + f, + "Receiver Type: {} Whitening filter: {} Corrections: {}", + self.receiver_type, self.whitening_filter, corr + ) + } +} diff --git a/src/timestep/mod.rs b/src/timestep/mod.rs index 23e300e..0e562f8 100644 --- a/src/timestep/mod.rs +++ b/src/timestep/mod.rs @@ -14,10 +14,14 @@ use std::fmt; #[cfg(test)] mod test; +#[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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct TimeStep { /// UNIX time (in milliseconds to avoid floating point inaccuracy) pub unix_time_ms: u64, diff --git a/src/voltage_context/mod.rs b/src/voltage_context/mod.rs index 793edd3..e3379d8 100644 --- a/src/voltage_context/mod.rs +++ b/src/voltage_context/mod.rs @@ -15,6 +15,9 @@ use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use std::path::Path; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; + #[cfg(feature = "python")] mod python; @@ -24,8 +27,9 @@ 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))] #[derive(Debug)] -#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] pub struct VoltageContext { /// Observation Metadata obtained from the metafits file pub metafits_context: MetafitsContext, @@ -136,7 +140,8 @@ pub struct VoltageContext { /// voltage number with a voltage batch number and HDU index. The voltage /// number, batch number and HDU index are everything needed to find the /// correct HDU out of all voltage files. - pub voltage_time_map: VoltageFileTimeMap, + #[allow(dead_code)] + pub(crate) voltage_time_map: VoltageFileTimeMap, } impl VoltageContext { diff --git a/src/voltage_context/python.rs b/src/voltage_context/python.rs index 706b560..ebebf96 100644 --- a/src/voltage_context/python.rs +++ b/src/voltage_context/python.rs @@ -13,25 +13,22 @@ use ndarray::Dim; use numpy::PyArray; #[cfg(feature = "python")] use pyo3::prelude::*; - #[cfg(feature = "python")] -#[pymethods] +use pyo3_stub_gen_derive::gen_stub_pymethods; + +#[cfg_attr(feature = "python", gen_stub_pymethods)] +#[cfg_attr(feature = "python", pymethods)] impl VoltageContext { - /// From a path to a metafits file and paths to voltage files, create an `VoltageContext`. - /// - /// # Arguments - /// - /// * `metafits_filename` - filename of metafits file. - /// - /// * `voltage_filenames` - list of filenames of voltage files. + /// From a path to a metafits file and paths to voltage files, create a `VoltageContext`. /// + /// Args: + /// metafits_filename (str): filename of metafits file as a path or string. + /// voltage_filenames (list[str]): list of filenames of voltage files. /// - /// # Returns - /// - /// * A populated VoltageContext object if Ok. - /// + /// Returns: + /// voltage_context (VoltageContext): a populated VoltageContext object if Ok. #[new] - #[pyo3(signature = (metafits_filename, voltage_filenames))] + #[pyo3(signature = (metafits_filename, voltage_filenames), text_signature = "(metafits_filename: str, mwa_version: list[voltage_filenames])")] fn pyo3_new(metafits_filename: PyObject, voltage_filenames: Vec) -> PyResult { // Convert the voltage filenames. let voltage_filenames: Vec = voltage_filenames @@ -43,52 +40,35 @@ impl VoltageContext { Ok(c) } - /// For a given list of voltage coarse channel indices, return a list of the center - /// frequencies for all the fine channels in the given coarse channels. - /// - /// # Arguments + /// For a given list of voltage coarse channel indices, return a list of the center frequencies for all the fine channels in the given coarse channels. /// - /// * `volt_coarse_chan_indices` - a list containing voltage coarse channel indices - /// for which you want fine channels for. Does not need to be - /// contiguous. - /// - /// # Returns + /// Args: + /// volt_coarse_chan_indices (list[int]): a list containing correlator coarse channel indices for which you want fine channels for. Does not need to be contiguous. /// - /// * a list of floats containing the centre sky frequencies of all the fine channels for the - /// given coarse channels. - /// - #[pyo3( - name = "get_fine_chan_freqs_hz_array", - signature = (volt_coarse_chan_indices) - )] + /// Returns: + /// fine_chan_freqs_hz_array (list[float]): a vector of floats containing the centre sky frequencies of all the fine channels for the given coarse channels. + #[pyo3(name = "get_fine_chan_freqs_hz_array")] fn pyo3_get_fine_chan_freqs_hz_array(&self, volt_coarse_chan_indices: Vec) -> Vec { self.get_fine_chan_freqs_hz_array(&volt_coarse_chan_indices) } /// Read a single timestep / coarse channel worth of data /// - /// # Arguments - /// - /// * `volt_timestep_index` - index within the timestep array for the desired timestep. This corresponds - /// to the element within VoltageContext.timesteps. For mwa legacy each index - /// represents 1 second increments, for mwax it is 8 second increments. - /// - /// * `volt_coarse_chan_index` - index within the coarse_chan array for the desired coarse channel. This corresponds - /// to the element within VoltageContext.coarse_chans. + /// Args: + /// volt_timestep_index (int): index within the timestep array for the desired timestep. This corresponds to the element within VoltageContext.timesteps. For mwa legacy each index represents 1 second increments, for mwax it is 8 second increments. + /// volt_coarse_chan_index (int): index within the coarse_chan array for the desired coarse channel. This corresponds to the element within VoltageContext.coarse_chans. /// - /// # Returns - /// - /// * An ndarray of signed bytes containing the data, if Ok. + /// Returns: + /// data (numpy.typing.NDArray[numpy.int8]): A 6 dimensional ndarray of signed bytes containing the data, if Ok. /// /// NOTE: The shape of the ndarray is different between LegacyVCS and MWAX VCS - /// Legacy: [second][time sample][chan][ant][pol][complexity] - /// where complexity is a byte (first 4 bits for real, second 4 bits for imaginary) in 2's compliment - /// - /// MWAX : [second][voltage_block][antenna][pol][sample][r,i] + /// Legacy: [second],[time sample],[chan],[ant],[pol],[complexity] + /// where complexity is a byte (first 4 bits for real, second 4 bits for imaginary) in 2's compliment + /// MWAX : [second],[voltage_block],[antenna],[pol],[sample],[r,i] /// #[pyo3( name = "read_file", - signature = (volt_timestep_index, volt_coarse_chan_index) + text_signature = "(self, volt_timestep_index, volt_coarse_chan_index)" )] fn pyo3_read_file<'py>( &self, @@ -146,27 +126,21 @@ impl VoltageContext { /// Read a single or multiple seconds of data for a coarse channel /// - /// # Arguments + /// Args: + /// gps_second_start (int): GPS second within the observation to start returning data. + /// gps_second_count (int): number of seconds of data to return. + /// volt_coarse_chan_index (int): index within the coarse_chan array for the desired coarse channel. This corresponds to the element within VoltageContext.coarse_chans. /// - /// * `gps_second_start` - GPS second within the observation to start returning data. - /// - /// * `gps_second_count` - number of seconds of data to return. - /// - /// * `volt_coarse_chan_index` - index within the coarse_chan array for the desired coarse channel. This corresponds - /// to the element within VoltageContext.coarse_chans. - /// - /// # Returns - /// - /// * An ndarray of signed bytes containing the data, if Ok. + /// Returns: + /// data (numpy.typing.NDArray[numpy.int8]): A 6 dimensional ndarray of signed bytes containing the data, if Ok. /// /// NOTE: The shape is different between LegacyVCS and MWAX VCS - /// Legacy: [second][time sample][chan][ant][pol][complexity] - /// where complexity is a byte (first 4 bits for real, second 4 bits for imaginary) in 2's compliment - /// - /// MWAX : [second][voltage_block][antenna][pol][sample][r,i] + /// Legacy: [second],[time sample],[chan],[ant],[pol],[complexity] + /// where complexity is a byte (first 4 bits for real, second 4 bits for imaginary) in 2's compliment + /// MWAX : [second],[voltage_block],[antenna],[pol],[sample],[r,i] #[pyo3( name = "read_second", - signature = (gps_second_start, gps_second_count, volt_coarse_chan_index) + text_signature = "(self, gps_second_start, gps_second_count, volt_coarse_chan_index)" )] fn pyo3_read_second<'py>( &self, diff --git a/src/voltage_files/mod.rs b/src/voltage_files/mod.rs index 42477c1..141e1ac 100644 --- a/src/voltage_files/mod.rs +++ b/src/voltage_files/mod.rs @@ -14,6 +14,9 @@ use std::collections::HashSet; use std::fmt; use std::path::Path; +#[cfg(feature = "python")] +use pyo3_stub_gen_derive::gen_stub_pyclass; + #[cfg(test)] mod test; @@ -29,11 +32,15 @@ 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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass)] pub struct VoltageFileBatch { - pub gps_time_seconds: u64, // 1234567890 - pub voltage_files: Vec, // Vector storing the details of each voltage file in this batch + // GPS second of this observation. e.g. 1234567890 + pub gps_time_seconds: u64, + + /// Vector storing the details of each voltage file in this batch + pub voltage_files: Vec, } impl VoltageFileBatch { @@ -56,8 +63,9 @@ 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))] #[derive(Clone)] -#[cfg_attr(feature = "python", pyo3::pyclass)] pub struct VoltageFile { /// Filename of voltage file pub filename: String, diff --git a/tests/test_metafits_context.py b/tests/test_metafits_context.py index 7bc9301..039d7a9 100644 --- a/tests/test_metafits_context.py +++ b/tests/test_metafits_context.py @@ -84,11 +84,13 @@ def test_mwax_metafits_context_rf_inputs( # this tests lists assert len(mwax_mc.rf_inputs) == 256 + rfinput: mwalib.Rfinput = mwax_mc.rf_inputs[0] + # this tests strings - assert mwax_mc.rf_inputs[0].tile_name == "Tile051" + assert rfinput.tile_name == "Tile051" # this tests enums - assert mwax_mc.rf_inputs[0].pol == mwalib.Pol.X + assert rfinput.pol == mwalib.Pol.X def test_mwax_metafits_context_antennas( @@ -98,12 +100,14 @@ def test_mwax_metafits_context_antennas( assert len(mwax_mc.antennas) == 128 assert mwax_mc.num_ants == 128 + ant: mwalib.Antenna = mwax_mc.antennas[0] + # this tests strings - assert mwax_mc.antennas[0].tile_name == "Tile051" + assert ant.tile_name == "Tile051" # this tests enums and objects as attributes - assert mwax_mc.antennas[0].rfinput_x.pol == mwalib.Pol.X - assert mwax_mc.antennas[0].rfinput_y.pol == mwalib.Pol.Y + assert ant.rfinput_x.pol == mwalib.Pol.X + assert ant.rfinput_y.pol == mwalib.Pol.Y def test_mwax_metafits_context_baselines(