Skip to main content

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