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 six functions:
143/// * `read_[name]` - reads a file, raising an error on a corrupted file
144/// * `read_[name]_lax` - reads a file, returning the records and the byte where corruption starts, if corrupted.
145/// * `read_[name]_bytes` - reads from bytes, similar to `read_[name]`
146/// * `read_[name]_bytes_lax` - reads from bytes, similar to `read_[name]_lax`
147/// * `sniff_[name]` - reads only the first record from file.
148/// * `read_[name]_metadata` - reads only the metadata from records in a file.
149/// reading, respectively.
150macro_rules! read_py {
151    (
152        $name:ident,
153        $py_name:literal,
154        $lax_name:literal,
155        $bytes_name:literal,
156        $lax_bytes_name:literal,
157        $sniff_name:literal,
158        $metadata_name:literal
159    ) => {
160        paste! {
161            #[doc = "Reads a `" $name:upper "` file, returning a list of dictionaries containing the fields." ]
162            #[pyfunction]
163            #[pyo3(name = $py_name)]
164            #[pyo3(text_signature = "(infile: str, /)")]
165            fn [< read_ $name _py >](infile: PathBuf) -> PyResult<Vec<IndexMap<String, DmapField>>> {
166                Ok([< $name:camel Record >]::read_file(&infile)
167                    .map_err(PyErr::from)?
168                    .into_iter()
169                    .map(|rec| rec.inner())
170                    .collect()
171                )
172            }
173
174            #[doc = "Reads a `" $name:upper "` file, returning a tuple of" ]
175            #[doc = "(list of dictionaries containing the fields, byte where first corrupted record starts). "]
176            #[pyfunction]
177            #[pyo3(name = $lax_name)]
178            #[pyo3(text_signature = "(infile: str, /)")]
179            fn [< read_ $name _lax_py >](
180                infile: PathBuf,
181            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
182                let result = [< $name:camel Record >]::read_file_lax(&infile).map_err(PyErr::from)?;
183                Ok((
184                    result.0.into_iter().map(|rec| rec.inner()).collect(),
185                    result.1,
186                ))
187            }
188
189            #[doc = "Read in `" $name:upper "` records from bytes, returning `List[Dict]` of the records." ]
190            #[pyfunction]
191            #[pyo3(name = $bytes_name)]
192            #[pyo3(text_signature = "(buf: bytes, /)")]
193            fn [< read_ $name _bytes_py >](bytes: &[u8]) -> PyResult<Vec<IndexMap<String, DmapField>>> {
194                Ok([< $name:camel Record >]::read_records(bytes)?
195                    .into_iter()
196                    .map(|rec| rec.inner())
197                    .collect()
198                )
199            }
200
201            #[doc = "Reads a `" $name:upper "` file, returning a tuple of" ]
202            #[doc = "(list of dictionaries containing the fields, byte where first corrupted record starts). "]
203            #[pyfunction]
204            #[pyo3(name = $lax_bytes_name)]
205            #[pyo3(text_signature = "(buf: bytes, /)")]
206            fn [< read_ $name _bytes_lax_py >](
207                bytes: &[u8],
208            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
209                let result = [< $name:camel Record >]::read_records_lax(bytes).map_err(PyErr::from)?;
210                Ok((
211                    result.0.into_iter().map(|rec| rec.inner()).collect(),
212                    result.1,
213                ))
214            }
215
216            #[doc = "Reads a `" $name:upper "` file, returning the first record." ]
217            #[pyfunction]
218            #[pyo3(name = $sniff_name)]
219            #[pyo3(text_signature = "(infile: str, /)")]
220            fn [< sniff_ $name _py >](infile: PathBuf) -> PyResult<IndexMap<String, DmapField>> {
221                Ok([< $name:camel Record >]::sniff_file(&infile)
222                    .map_err(PyErr::from)?
223                    .inner()
224                )
225            }
226
227            #[doc = "Reads a `" $name:upper "` file, returning a list of dictionaries containing the only the metadata fields." ]
228            #[pyfunction]
229            #[pyo3(name = $metadata_name)]
230            #[pyo3(text_signature = "(infile: str, /)")]
231            fn [< read_ $name _metadata_py >](infile: PathBuf) -> PyResult<Vec<IndexMap<String, DmapField>>> {
232                Ok([< $name:camel Record >]::read_file_metadata(&infile)
233                    .map_err(PyErr::from)?
234                )
235            }
236        }
237    }
238}
239
240read_py!(
241    iqdat,
242    "read_iqdat",
243    "read_iqdat_lax",
244    "read_iqdat_bytes",
245    "read_iqdat_bytes_lax",
246    "sniff_iqdat",
247    "read_iqdat_metadata"
248);
249read_py!(
250    rawacf,
251    "read_rawacf",
252    "read_rawacf_lax",
253    "read_rawacf_bytes",
254    "read_rawacf_bytes_lax",
255    "sniff_rawacf",
256    "read_rawacf_metadata"
257);
258read_py!(
259    fitacf,
260    "read_fitacf",
261    "read_fitacf_lax",
262    "read_fitacf_bytes",
263    "read_fitacf_bytes_lax",
264    "sniff_fitacf",
265    "read_fitacf_metadata"
266);
267read_py!(
268    grid,
269    "read_grid",
270    "read_grid_lax",
271    "read_grid_bytes",
272    "read_grid_bytes_lax",
273    "sniff_grid",
274    "read_grid_metadata"
275);
276read_py!(
277    map,
278    "read_map",
279    "read_map_lax",
280    "read_map_bytes",
281    "read_map_bytes_lax",
282    "sniff_map",
283    "read_map_metadata"
284);
285read_py!(
286    snd,
287    "read_snd",
288    "read_snd_lax",
289    "read_snd_bytes",
290    "read_snd_bytes_lax",
291    "sniff_snd",
292    "read_snd_metadata"
293);
294read_py!(
295    dmap,
296    "read_dmap",
297    "read_dmap_lax",
298    "read_dmap_bytes",
299    "read_dmap_bytes_lax",
300    "sniff_dmap",
301    "read_dmap_metadata"
302);
303
304/// Checks that a list of dictionaries contains DMAP records, then appends to outfile.
305///
306/// **NOTE:** No type checking is done, so the fields may not be written as the expected
307/// DMAP type, e.g. `stid` might be written one byte instead of two as this function
308/// does not know that typically `stid` is two bytes.
309#[pyfunction]
310#[pyo3(name = "write_dmap")]
311#[pyo3(text_signature = "(recs: list[dict], outfile: str, /)")]
312fn write_dmap_py(recs: Vec<IndexMap<String, DmapField>>, outfile: PathBuf) -> PyResult<()> {
313    try_write_dmap(recs, &outfile).map_err(PyErr::from)
314}
315
316/// Checks that a list of dictionaries contains valid DMAP records, then converts them to bytes.
317/// Returns `list[bytes]`, one entry per record.
318///
319/// **NOTE:** No type checking is done, so the fields may not be written as the expected
320/// DMAP type, e.g. `stid` might be written one byte instead of two as this function
321/// does not know that typically `stid` is two bytes.
322#[pyfunction]
323#[pyo3(name = "write_dmap_bytes")]
324#[pyo3(text_signature = "(recs: list[dict], /)")]
325fn write_dmap_bytes_py(py: Python, recs: Vec<IndexMap<String, DmapField>>) -> PyResult<Py<PyAny>> {
326    let bytes = DmapRecord::try_into_bytes(recs).map_err(PyErr::from)?;
327    Ok(PyBytes::new(py, &bytes).into())
328}
329
330/// Generates functions exposed to the Python API for writing specific file types.
331macro_rules! write_py {
332    ($name:ident, $fn_name:literal, $bytes_name:literal) => {
333        paste! {
334            #[doc = "Checks that a list of dictionaries contains valid `" $name:upper "` records, then appends to outfile." ]
335            #[pyfunction]
336            #[pyo3(name = $fn_name)]
337            #[pyo3(text_signature = "(recs: list[dict], outfile: str, /)")]
338            fn [< write_ $name _py >](recs: Vec<IndexMap<String, DmapField>>, outfile: PathBuf) -> PyResult<()> {
339                [< try_write_ $name >](recs, &outfile).map_err(PyErr::from)
340            }
341
342            #[doc = "Checks that a list of dictionaries contains valid `" $name:upper "` records, then converts them to bytes." ]
343            #[doc = "Returns `list[bytes]`, one entry per record." ]
344            #[pyfunction]
345            #[pyo3(name = $bytes_name)]
346            #[pyo3(text_signature = "(recs: list[dict], /)")]
347            fn [< write_ $name _bytes_py >](py: Python, recs: Vec<IndexMap<String, DmapField>>) -> PyResult<Py<PyAny>> {
348                let bytes = [< $name:camel Record >]::try_into_bytes(recs).map_err(PyErr::from)?;
349                Ok(PyBytes::new(py, &bytes).into())
350            }
351        }
352    }
353}
354
355// **NOTE** dmap type not included in this list, since it has a more descriptive docstring.
356write_py!(iqdat, "write_iqdat", "write_iqdat_bytes");
357write_py!(rawacf, "write_rawacf", "write_rawacf_bytes");
358write_py!(fitacf, "write_fitacf", "write_fitacf_bytes");
359write_py!(grid, "write_grid", "write_grid_bytes");
360write_py!(map, "write_map", "write_map_bytes");
361write_py!(snd, "write_snd", "write_snd_bytes");
362
363/// Functions for SuperDARN DMAP file format I/O.
364#[pymodule]
365fn dmap_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
366    // Strict read functions
367    m.add_function(wrap_pyfunction!(read_dmap_py, m)?)?;
368    m.add_function(wrap_pyfunction!(read_iqdat_py, m)?)?;
369    m.add_function(wrap_pyfunction!(read_rawacf_py, m)?)?;
370    m.add_function(wrap_pyfunction!(read_fitacf_py, m)?)?;
371    m.add_function(wrap_pyfunction!(read_snd_py, m)?)?;
372    m.add_function(wrap_pyfunction!(read_grid_py, m)?)?;
373    m.add_function(wrap_pyfunction!(read_map_py, m)?)?;
374
375    // Lax read functions
376    m.add_function(wrap_pyfunction!(read_dmap_lax_py, m)?)?;
377    m.add_function(wrap_pyfunction!(read_iqdat_lax_py, m)?)?;
378    m.add_function(wrap_pyfunction!(read_rawacf_lax_py, m)?)?;
379    m.add_function(wrap_pyfunction!(read_fitacf_lax_py, m)?)?;
380    m.add_function(wrap_pyfunction!(read_snd_lax_py, m)?)?;
381    m.add_function(wrap_pyfunction!(read_grid_lax_py, m)?)?;
382    m.add_function(wrap_pyfunction!(read_map_lax_py, m)?)?;
383
384    // Read functions from byte buffer
385    m.add_function(wrap_pyfunction!(read_dmap_bytes_py, m)?)?;
386    m.add_function(wrap_pyfunction!(read_iqdat_bytes_py, m)?)?;
387    m.add_function(wrap_pyfunction!(read_rawacf_bytes_py, m)?)?;
388    m.add_function(wrap_pyfunction!(read_fitacf_bytes_py, m)?)?;
389    m.add_function(wrap_pyfunction!(read_snd_bytes_py, m)?)?;
390    m.add_function(wrap_pyfunction!(read_grid_bytes_py, m)?)?;
391    m.add_function(wrap_pyfunction!(read_map_bytes_py, m)?)?;
392
393    // Lax read functions from byte buffer
394    m.add_function(wrap_pyfunction!(read_dmap_bytes_lax_py, m)?)?;
395    m.add_function(wrap_pyfunction!(read_iqdat_bytes_lax_py, m)?)?;
396    m.add_function(wrap_pyfunction!(read_rawacf_bytes_lax_py, m)?)?;
397    m.add_function(wrap_pyfunction!(read_fitacf_bytes_lax_py, m)?)?;
398    m.add_function(wrap_pyfunction!(read_snd_bytes_lax_py, m)?)?;
399    m.add_function(wrap_pyfunction!(read_grid_bytes_lax_py, m)?)?;
400    m.add_function(wrap_pyfunction!(read_map_bytes_lax_py, m)?)?;
401
402    // Write functions
403    m.add_function(wrap_pyfunction!(write_dmap_py, m)?)?;
404    m.add_function(wrap_pyfunction!(write_iqdat_py, m)?)?;
405    m.add_function(wrap_pyfunction!(write_rawacf_py, m)?)?;
406    m.add_function(wrap_pyfunction!(write_fitacf_py, m)?)?;
407    m.add_function(wrap_pyfunction!(write_grid_py, m)?)?;
408    m.add_function(wrap_pyfunction!(write_map_py, m)?)?;
409    m.add_function(wrap_pyfunction!(write_snd_py, m)?)?;
410
411    // Convert records to bytes
412    m.add_function(wrap_pyfunction!(write_dmap_bytes_py, m)?)?;
413    m.add_function(wrap_pyfunction!(write_iqdat_bytes_py, m)?)?;
414    m.add_function(wrap_pyfunction!(write_rawacf_bytes_py, m)?)?;
415    m.add_function(wrap_pyfunction!(write_fitacf_bytes_py, m)?)?;
416    m.add_function(wrap_pyfunction!(write_snd_bytes_py, m)?)?;
417    m.add_function(wrap_pyfunction!(write_grid_bytes_py, m)?)?;
418    m.add_function(wrap_pyfunction!(write_map_bytes_py, m)?)?;
419
420    // Sniff the first record
421    m.add_function(wrap_pyfunction!(sniff_dmap_py, m)?)?;
422    m.add_function(wrap_pyfunction!(sniff_iqdat_py, m)?)?;
423    m.add_function(wrap_pyfunction!(sniff_rawacf_py, m)?)?;
424    m.add_function(wrap_pyfunction!(sniff_fitacf_py, m)?)?;
425    m.add_function(wrap_pyfunction!(sniff_snd_py, m)?)?;
426    m.add_function(wrap_pyfunction!(sniff_grid_py, m)?)?;
427    m.add_function(wrap_pyfunction!(sniff_map_py, m)?)?;
428
429    // Read only the metadata from files
430    m.add_function(wrap_pyfunction!(read_dmap_metadata_py, m)?)?;
431    m.add_function(wrap_pyfunction!(read_iqdat_metadata_py, m)?)?;
432    m.add_function(wrap_pyfunction!(read_rawacf_metadata_py, m)?)?;
433    m.add_function(wrap_pyfunction!(read_fitacf_metadata_py, m)?)?;
434    m.add_function(wrap_pyfunction!(read_snd_metadata_py, m)?)?;
435    m.add_function(wrap_pyfunction!(read_grid_metadata_py, m)?)?;
436    m.add_function(wrap_pyfunction!(read_map_metadata_py, m)?)?;
437
438    Ok(())
439}