dmap/
lib.rs

1//! A library for SuperDARN DMAP file I/O.
2//!
3//! [![github]](https://github.com/SuperDARNCanada/dmap) [![crates-io]](https://crates.io/crates/darn-dmap) [![docs-rs]](crate)
4//!
5//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
6//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
7//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
8//!
9//! <br>
10//!
11//! This library also has a Python API using pyo3.
12//!
13//! For more information about DMAP files, see [RST](https://radar-software-toolkit-rst.readthedocs.io/en/latest/)
14//! or [pyDARNio](https://pydarnio.readthedocs.io/en/latest/).
15//!
16//! The main feature of this crate is the [`Record`] trait, which defines a valid DMAP record and functions for
17//! reading from and writing to byte streams. The SuperDARN file formats IQDAT, RAWACF, FITACF, GRID, MAP, and SND are
18//! all supported with structs that implement [`Record`], namely:
19//!
20//! - [`IqdatRecord`]
21//! - [`RawacfRecord`]
22//! - [`FitacfRecord`]
23//! - [`GridRecord`]
24//! - [`MapRecord`]
25//! - [`SndRecord`]
26//!
27//! Each struct has a list of required and optional fields that it uses to verify the integrity of the record.
28//! Only fields listed in the required and optional lists are allowed, no required field can be missing, and
29//! each field has an expected primitive type. Additionally, each format has groupings of vector fields which
30//! must all share the same shape; e.g. `acfd` and `xcfd` in a RAWACF file.
31//!
32//! There is also a generic [`DmapRecord`] struct which has no knowledge of required or optional fields. When reading from
33//! a byte stream, the parsed data will be identical when using both a specific format like [`RawacfRecord`] and the generic
34//! [`DmapRecord`]; however, when writing to a byte stream, the output may differ. Since [`DmapRecord`] has no knowledge of
35//! the expected primitive type for each field, it defaults to a type that fits the data. For example, the `stid` field may
36//! be saved as an `i8` when using [`DmapRecord`] instead of an `i16` which [`RawacfRecord`] specifies it must be.
37//!
38//! <div class="note">
39//! Each type of record has a specific field ordering hard-coded by this library. This is the order in which fields are written to file,
40//! and may not match the ordering of fields generated by RST. This also means that round-trip I/O (i.e. reading a file and
41//! writing back out to a new file) is not guaranteed to generate an identical file; however, it is guaranteed that all the
42//! information is the same, just not necessarily in the same order.
43//! </div>
44//!
45//! <br>
46//!
47//! # Examples
48//!
49//! Convenience functions for reading from and writing to a file exist to simplify the most common use cases.
50//! This is defined by [`Record::read_file`]
51//! ```
52//! use dmap::*;
53//! use std::path::PathBuf;
54//!
55//! # fn main() -> Result<(), DmapError> {
56//! let path = PathBuf::from("tests/test_files/test.rawacf");
57//! let rawacf_data = RawacfRecord::read_file(&path)?;
58//! let unchecked_data = DmapRecord::read_file(&path)?;
59//!
60//! assert_eq!(rawacf_data.len(), unchecked_data.len());
61//! assert_eq!(rawacf_data[0].get(&"stid".to_string()), unchecked_data[0].get(&"stid".to_string()));
62//!
63//! // Write the records to a file
64//! let out_path = PathBuf::from("tests/test_files/output.rawacf");
65//! RawacfRecord::write_to_file(&rawacf_data, &out_path)?;
66//! # std::fs::remove_file(out_path)?;
67//! #    Ok(())
68//! # }
69//! ```
70//! You can read from anything that implements the `Read` trait using the functions exposed by the [`Record`] trait.
71//! Detection and decompression of bz2 is also conducted automatically.
72//! ```
73//! use dmap::*;
74//! use std::fs::File;
75//! use itertools::izip;
76//!
77//! # fn main() -> Result<(), DmapError> {
78//! let file = File::open("tests/test_files/test.rawacf.bz2")?;  // `File` implements the `Read` trait
79//! let rawacf_data = RawacfRecord::read_records(file)?;
80//!
81//! let uncompressed_data = RawacfRecord::read_file("tests/test_files/test.rawacf")?;
82//! assert_eq!(rawacf_data.len(), uncompressed_data.len());
83//! for (left, right) in izip!(rawacf_data, uncompressed_data) {
84//!     assert_eq!(left, right)
85//! }
86//! #     Ok(())
87//! # }
88//! ```
89
90pub(crate) mod compression;
91pub mod error;
92pub mod formats;
93pub(crate) mod io;
94pub mod record;
95pub mod types;
96
97pub use crate::error::DmapError;
98pub use crate::formats::dmap::DmapRecord;
99pub use crate::formats::fitacf::FitacfRecord;
100pub use crate::formats::grid::GridRecord;
101pub use crate::formats::iqdat::IqdatRecord;
102pub use crate::formats::map::MapRecord;
103pub use crate::formats::rawacf::RawacfRecord;
104pub use crate::formats::snd::SndRecord;
105pub use crate::record::Record;
106use crate::types::DmapField;
107use indexmap::IndexMap;
108use paste::paste;
109use pyo3::prelude::*;
110use pyo3::types::PyBytes;
111use std::path::{Path, PathBuf};
112
113/// This macro generates a function for attempting to convert `Vec<IndexMap>` to `Vec<$type>` and write it to file.
114macro_rules! write_rust {
115    ($type:ident) => {
116        paste! {
117            #[doc = "Attempts to convert `recs` to `" $type:camel Record "` then append to `outfile`."]
118            #[doc = ""]
119            #[doc = "# Errors"]
120            #[doc = "if any of the `IndexMap`s are unable to be interpreted as a `" $type:camel Record "`, or there is an issue writing to file."]
121            pub fn [< try_write_ $type >]<P: AsRef<Path>>(
122                recs: Vec<IndexMap<String, DmapField>>,
123                outfile: P,
124            ) -> Result<(), DmapError> {
125                let bytes = [< $type:camel Record >]::try_into_bytes(recs)?;
126                crate::io::bytes_to_file(bytes, outfile).map_err(DmapError::from)
127            }
128        }
129    }
130}
131
132write_rust!(iqdat);
133write_rust!(rawacf);
134write_rust!(fitacf);
135write_rust!(grid);
136write_rust!(map);
137write_rust!(snd);
138write_rust!(dmap);
139
140/// Creates functions for reading DMAP files for the Python API.
141///
142/// Generates two functions: `read_[type]` and `read_[type]_lax`, for strict and lax
143/// reading, respectively.
144macro_rules! read_py {
145    ($name:ident, $py_name:literal, $lax_name:literal, $bytes_name:literal, $lax_bytes_name:literal, $sniff_name:literal) => {
146        paste! {
147            #[doc = "Reads a `" $name:upper "` file, returning a list of dictionaries containing the fields." ]
148            #[pyfunction]
149            #[pyo3(name = $py_name)]
150            #[pyo3(text_signature = "(infile: str, /)")]
151            fn [< read_ $name _py >](infile: PathBuf) -> PyResult<Vec<IndexMap<String, DmapField>>> {
152                Ok([< $name:camel Record >]::read_file(&infile)
153                    .map_err(PyErr::from)?
154                    .into_iter()
155                    .map(|rec| rec.inner())
156                    .collect()
157                )
158            }
159
160            #[doc = "Reads a `" $name:upper "` file, returning a tuple of" ]
161            #[doc = "(list of dictionaries containing the fields, byte where first corrupted record starts). "]
162            #[pyfunction]
163            #[pyo3(name = $lax_name)]
164            #[pyo3(text_signature = "(infile: str, /)")]
165            fn [< read_ $name _lax_py >](
166                infile: PathBuf,
167            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
168                let result = [< $name:camel Record >]::read_file_lax(&infile).map_err(PyErr::from)?;
169                Ok((
170                    result.0.into_iter().map(|rec| rec.inner()).collect(),
171                    result.1,
172                ))
173            }
174
175            #[doc = "Read in `" $name:upper "` records from bytes, returning `List[Dict]` of the records." ]
176            #[pyfunction]
177            #[pyo3(name = $bytes_name)]
178            #[pyo3(text_signature = "(buf: bytes, /)")]
179            fn [< read_ $name _bytes_py >](bytes: &[u8]) -> PyResult<Vec<IndexMap<String, DmapField>>> {
180                Ok([< $name:camel Record >]::read_records(bytes)?
181                    .into_iter()
182                    .map(|rec| rec.inner())
183                    .collect()
184                )
185            }
186
187            #[doc = "Reads a `" $name:upper "` file, returning a tuple of" ]
188            #[doc = "(list of dictionaries containing the fields, byte where first corrupted record starts). "]
189            #[pyfunction]
190            #[pyo3(name = $lax_bytes_name)]
191            #[pyo3(text_signature = "(buf: bytes, /)")]
192            fn [< read_ $name _bytes_lax_py >](
193                bytes: &[u8],
194            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
195                let result = [< $name:camel Record >]::read_records_lax(bytes).map_err(PyErr::from)?;
196                Ok((
197                    result.0.into_iter().map(|rec| rec.inner()).collect(),
198                    result.1,
199                ))
200            }
201
202            #[doc = "Reads a `" $name:upper "` file, returning the first record." ]
203            #[pyfunction]
204            #[pyo3(name = $sniff_name)]
205            #[pyo3(text_signature = "(infile: str, /)")]
206            fn [< sniff_ $name _py >](infile: PathBuf) -> PyResult<IndexMap<String, DmapField>> {
207                Ok([< $name:camel Record >]::sniff_file(&infile)
208                    .map_err(PyErr::from)?
209                    .inner()
210                )
211            }
212        }
213    }
214}
215
216read_py!(
217    iqdat,
218    "read_iqdat",
219    "read_iqdat_lax",
220    "read_iqdat_bytes",
221    "read_iqdat_bytes_lax",
222    "sniff_iqdat"
223);
224read_py!(
225    rawacf,
226    "read_rawacf",
227    "read_rawacf_lax",
228    "read_rawacf_bytes",
229    "read_rawacf_bytes_lax",
230    "sniff_rawacf"
231);
232read_py!(
233    fitacf,
234    "read_fitacf",
235    "read_fitacf_lax",
236    "read_fitacf_bytes",
237    "read_fitacf_bytes_lax",
238    "sniff_fitacf"
239);
240read_py!(
241    grid,
242    "read_grid",
243    "read_grid_lax",
244    "read_grid_bytes",
245    "read_grid_bytes_lax",
246    "sniff_grid"
247);
248read_py!(
249    map,
250    "read_map",
251    "read_map_lax",
252    "read_map_bytes",
253    "read_map_bytes_lax",
254    "sniff_map"
255);
256read_py!(
257    snd,
258    "read_snd",
259    "read_snd_lax",
260    "read_snd_bytes",
261    "read_snd_bytes_lax",
262    "sniff_snd"
263);
264read_py!(
265    dmap,
266    "read_dmap",
267    "read_dmap_lax",
268    "read_dmap_bytes",
269    "read_dmap_bytes_lax",
270    "sniff_dmap"
271);
272
273/// Checks that a list of dictionaries contains DMAP records, then appends to outfile.
274///
275/// **NOTE:** No type checking is done, so the fields may not be written as the expected
276/// DMAP type, e.g. `stid` might be written one byte instead of two as this function
277/// does not know that typically `stid` is two bytes.
278#[pyfunction]
279#[pyo3(name = "write_dmap")]
280#[pyo3(text_signature = "(recs: list[dict], outfile: str, /)")]
281fn write_dmap_py(recs: Vec<IndexMap<String, DmapField>>, outfile: PathBuf) -> PyResult<()> {
282    try_write_dmap(recs, &outfile).map_err(PyErr::from)
283}
284
285/// Checks that a list of dictionaries contains valid DMAP records, then converts them to bytes.
286/// Returns `list[bytes]`, one entry per record.
287///
288/// **NOTE:** No type checking is done, so the fields may not be written as the expected
289/// DMAP type, e.g. `stid` might be written one byte instead of two as this function
290/// does not know that typically `stid` is two bytes.
291#[pyfunction]
292#[pyo3(name = "write_dmap_bytes")]
293#[pyo3(text_signature = "(recs: list[dict], /)")]
294fn write_dmap_bytes_py(py: Python, recs: Vec<IndexMap<String, DmapField>>) -> PyResult<Py<PyAny>> {
295    let bytes = DmapRecord::try_into_bytes(recs).map_err(PyErr::from)?;
296    Ok(PyBytes::new(py, &bytes).into())
297}
298
299/// Generates functions exposed to the Python API for writing specific file types.
300macro_rules! write_py {
301    ($name:ident, $fn_name:literal, $bytes_name:literal) => {
302        paste! {
303            #[doc = "Checks that a list of dictionaries contains valid `" $name:upper "` records, then appends to outfile." ]
304            #[pyfunction]
305            #[pyo3(name = $fn_name)]
306            #[pyo3(text_signature = "(recs: list[dict], outfile: str, /)")]
307            fn [< write_ $name _py >](recs: Vec<IndexMap<String, DmapField>>, outfile: PathBuf) -> PyResult<()> {
308                [< try_write_ $name >](recs, &outfile).map_err(PyErr::from)
309            }
310
311            #[doc = "Checks that a list of dictionaries contains valid `" $name:upper "` records, then converts them to bytes." ]
312            #[doc = "Returns `list[bytes]`, one entry per record." ]
313            #[pyfunction]
314            #[pyo3(name = $bytes_name)]
315            #[pyo3(text_signature = "(recs: list[dict], /)")]
316            fn [< write_ $name _bytes_py >](py: Python, recs: Vec<IndexMap<String, DmapField>>) -> PyResult<Py<PyAny>> {
317                let bytes = [< $name:camel Record >]::try_into_bytes(recs).map_err(PyErr::from)?;
318                Ok(PyBytes::new(py, &bytes).into())
319            }
320        }
321    }
322}
323
324// **NOTE** dmap type not included in this list, since it has a more descriptive docstring.
325write_py!(iqdat, "write_iqdat", "write_iqdat_bytes");
326write_py!(rawacf, "write_rawacf", "write_rawacf_bytes");
327write_py!(fitacf, "write_fitacf", "write_fitacf_bytes");
328write_py!(grid, "write_grid", "write_grid_bytes");
329write_py!(map, "write_map", "write_map_bytes");
330write_py!(snd, "write_snd", "write_snd_bytes");
331
332/// Functions for SuperDARN DMAP file format I/O.
333#[pymodule]
334fn dmap_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
335    // Strict read functions
336    m.add_function(wrap_pyfunction!(read_dmap_py, m)?)?;
337    m.add_function(wrap_pyfunction!(read_iqdat_py, m)?)?;
338    m.add_function(wrap_pyfunction!(read_rawacf_py, m)?)?;
339    m.add_function(wrap_pyfunction!(read_fitacf_py, m)?)?;
340    m.add_function(wrap_pyfunction!(read_snd_py, m)?)?;
341    m.add_function(wrap_pyfunction!(read_grid_py, m)?)?;
342    m.add_function(wrap_pyfunction!(read_map_py, m)?)?;
343
344    // Lax read functions
345    m.add_function(wrap_pyfunction!(read_dmap_lax_py, m)?)?;
346    m.add_function(wrap_pyfunction!(read_iqdat_lax_py, m)?)?;
347    m.add_function(wrap_pyfunction!(read_rawacf_lax_py, m)?)?;
348    m.add_function(wrap_pyfunction!(read_fitacf_lax_py, m)?)?;
349    m.add_function(wrap_pyfunction!(read_snd_lax_py, m)?)?;
350    m.add_function(wrap_pyfunction!(read_grid_lax_py, m)?)?;
351    m.add_function(wrap_pyfunction!(read_map_lax_py, m)?)?;
352
353    // Read functions from byte buffer
354    m.add_function(wrap_pyfunction!(read_dmap_bytes_py, m)?)?;
355    m.add_function(wrap_pyfunction!(read_iqdat_bytes_py, m)?)?;
356    m.add_function(wrap_pyfunction!(read_rawacf_bytes_py, m)?)?;
357    m.add_function(wrap_pyfunction!(read_fitacf_bytes_py, m)?)?;
358    m.add_function(wrap_pyfunction!(read_snd_bytes_py, m)?)?;
359    m.add_function(wrap_pyfunction!(read_grid_bytes_py, m)?)?;
360    m.add_function(wrap_pyfunction!(read_map_bytes_py, m)?)?;
361
362    // Lax read functions from byte buffer
363    m.add_function(wrap_pyfunction!(read_dmap_bytes_lax_py, m)?)?;
364    m.add_function(wrap_pyfunction!(read_iqdat_bytes_lax_py, m)?)?;
365    m.add_function(wrap_pyfunction!(read_rawacf_bytes_lax_py, m)?)?;
366    m.add_function(wrap_pyfunction!(read_fitacf_bytes_lax_py, m)?)?;
367    m.add_function(wrap_pyfunction!(read_snd_bytes_lax_py, m)?)?;
368    m.add_function(wrap_pyfunction!(read_grid_bytes_lax_py, m)?)?;
369    m.add_function(wrap_pyfunction!(read_map_bytes_lax_py, m)?)?;
370
371    // Write functions
372    m.add_function(wrap_pyfunction!(write_dmap_py, m)?)?;
373    m.add_function(wrap_pyfunction!(write_iqdat_py, m)?)?;
374    m.add_function(wrap_pyfunction!(write_rawacf_py, m)?)?;
375    m.add_function(wrap_pyfunction!(write_fitacf_py, m)?)?;
376    m.add_function(wrap_pyfunction!(write_grid_py, m)?)?;
377    m.add_function(wrap_pyfunction!(write_map_py, m)?)?;
378    m.add_function(wrap_pyfunction!(write_snd_py, m)?)?;
379
380    // Convert records to bytes
381    m.add_function(wrap_pyfunction!(write_dmap_bytes_py, m)?)?;
382    m.add_function(wrap_pyfunction!(write_iqdat_bytes_py, m)?)?;
383    m.add_function(wrap_pyfunction!(write_rawacf_bytes_py, m)?)?;
384    m.add_function(wrap_pyfunction!(write_fitacf_bytes_py, m)?)?;
385    m.add_function(wrap_pyfunction!(write_snd_bytes_py, m)?)?;
386    m.add_function(wrap_pyfunction!(write_grid_bytes_py, m)?)?;
387    m.add_function(wrap_pyfunction!(write_map_bytes_py, m)?)?;
388
389    // Sniff the first record
390    m.add_function(wrap_pyfunction!(sniff_dmap_py, m)?)?;
391    m.add_function(wrap_pyfunction!(sniff_iqdat_py, m)?)?;
392    m.add_function(wrap_pyfunction!(sniff_rawacf_py, m)?)?;
393    m.add_function(wrap_pyfunction!(sniff_fitacf_py, m)?)?;
394    m.add_function(wrap_pyfunction!(sniff_snd_py, m)?)?;
395    m.add_function(wrap_pyfunction!(sniff_grid_py, m)?)?;
396    m.add_function(wrap_pyfunction!(sniff_map_py, m)?)?;
397
398    Ok(())
399}