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(crate) mod convenience;
92pub mod error;
93pub mod formats;
94pub(crate) mod io;
95pub mod record;
96pub mod types;
97mod parser;
98
99pub use crate::error::DmapError;
100pub use crate::formats::dmap::DmapRecord;
101pub use crate::formats::fitacf::FitacfRecord;
102pub use crate::formats::grid::GridRecord;
103pub use crate::formats::iqdat::IqdatRecord;
104pub use crate::formats::map::MapRecord;
105pub use crate::formats::rawacf::RawacfRecord;
106pub use crate::formats::snd::SndRecord;
107pub use crate::record::Record;
108use crate::types::DmapField;
109use indexmap::IndexMap;
110use paste::paste;
111use pyo3::prelude::*;
112use pyo3::types::PyBytes;
113use std::path::{Path, PathBuf};
114
115/// This macro generates a function for attempting to convert `Vec<IndexMap>` to `Vec<$type>` and write it to file.
116macro_rules! write_rust {
117    ($type:ident) => {
118        paste! {
119            #[doc = "Attempts to convert `recs` to `" $type:camel Record "` then append to `outfile`."]
120            #[doc = ""]
121            #[doc = "# Errors"]
122            #[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."]
123            pub fn [< try_write_ $type >]<P: AsRef<Path>>(
124                recs: Vec<IndexMap<String, DmapField>>,
125                outfile: P,
126                bz2: bool,
127            ) -> Result<(), DmapError> {
128                let bytes = [< $type:camel Record >]::try_into_bytes(recs)?;
129                crate::io::bytes_to_file(bytes, outfile, bz2).map_err(DmapError::from)
130            }
131        }
132    }
133}
134
135write_rust!(iqdat);
136write_rust!(rawacf);
137write_rust!(fitacf);
138write_rust!(grid);
139write_rust!(map);
140write_rust!(snd);
141write_rust!(dmap);
142
143/// Creates functions for reading DMAP files for the Python API.
144///
145/// Generates six functions:
146/// * `read_[name]` - reads a file, raising an error on a corrupted file.
147/// * `read_[name]_lax` - reads a file, returning the records and the byte where corruption starts, if corrupted.
148/// * `read_[name]_bytes` - reads from bytes, similar to `read_[name]`.
149/// * `read_[name]_bytes_lax` - reads from bytes, similar to `read_[name]_lax`.
150/// * `read_[name]_by_index` - reads only the nth record(s) from file.
151/// * `read_[name]_by_index_lax` - reads only the nth record(s) from file in the same manner as `read_[name]_lax`.
152/// * `read_[name]_metadata` - reads only the metadata from records in a file.
153/// reading, respectively.
154macro_rules! read_py {
155    (
156        $name:ident,
157        $py_name:literal,
158        $lax_name:literal,
159        $bytes_name:literal,
160        $lax_bytes_name:literal,
161        $sniff_name:literal,
162        $sniff_name_lax:literal,
163        $sniff_bytes:literal,
164        $sniff_bytes_lax:literal,
165        $metadata_name:literal,
166        $sniff_metadata:literal
167    ) => {
168        paste! {
169            #[doc = "Reads a `" $name:upper "` file, returning a list of dictionaries containing the fields." ]
170            #[pyfunction]
171            #[pyo3(name = $py_name)]
172            #[pyo3(text_signature = "(infile: str, /)")]
173            fn [< read_ $name _py >](infile: PathBuf) -> PyResult<Vec<IndexMap<String, DmapField>>> {
174                Ok([< $name:camel Record >]::read_file(&infile)
175                    .map_err(PyErr::from)?
176                    .into_iter()
177                    .map(|rec| rec.inner())
178                    .collect()
179                )
180            }
181
182            #[doc = "Reads a `" $name:upper "` file, returning a tuple of" ]
183            #[doc = "(list of dictionaries containing the fields, byte where first corrupted record starts). "]
184            #[pyfunction]
185            #[pyo3(name = $lax_name)]
186            #[pyo3(text_signature = "(infile: str, /)")]
187            fn [< read_ $name _lax_py >](
188                infile: PathBuf,
189            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
190                let result = [< $name:camel Record >]::read_file_lax(&infile).map_err(PyErr::from)?;
191                Ok((
192                    result.0.into_iter().map(|rec| rec.inner()).collect(),
193                    result.1,
194                ))
195            }
196
197            #[doc = "Read in `" $name:upper "` records from bytes, returning `List[Dict]` of the records." ]
198            #[pyfunction]
199            #[pyo3(name = $bytes_name)]
200            #[pyo3(text_signature = "(buf: bytes, /)")]
201            fn [< read_ $name _bytes_py >](bytes: &[u8]) -> PyResult<Vec<IndexMap<String, DmapField>>> {
202                Ok([< $name:camel Record >]::read_records(bytes)?
203                    .into_iter()
204                    .map(|rec| rec.inner())
205                    .collect()
206                )
207            }
208
209            #[doc = "Reads a `" $name:upper "` file, returning a tuple of" ]
210            #[doc = "(list of dictionaries containing the fields, byte where first corrupted record starts). "]
211            #[pyfunction]
212            #[pyo3(name = $lax_bytes_name)]
213            #[pyo3(text_signature = "(buf: bytes, /)")]
214            fn [< read_ $name _bytes_lax_py >](
215                bytes: &[u8],
216            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
217                let result = [< $name:camel Record >]::read_records_lax(bytes).map_err(PyErr::from)?;
218                Ok((
219                    result.0.into_iter().map(|rec| rec.inner()).collect(),
220                    result.1,
221                ))
222            }
223
224            #[doc = "Reads a `" $name:upper "` file, returning the `nth` record(s)." ]
225            #[pyfunction]
226            #[pyo3(name = $sniff_name)]
227            #[pyo3(text_signature = "(infile: str, indices: tuple[int], /)")]
228            fn [< read_ $name _by_indices_py >](infile: PathBuf, indices: Vec<i32>) -> PyResult<Vec<IndexMap<String, DmapField>>> {
229                Ok([< $name:camel Record >]::read_file_by_indices(&infile, &indices)
230                    .map_err(PyErr::from)?
231                    .into_iter()
232                    .map(|rec| rec.inner())
233                    .collect()
234                )
235            }
236
237            #[doc = "Reads a `" $name:upper "` file, returning the `nth` record(s), and the byte index where corruption starts, if applicable" ]
238            #[pyfunction]
239            #[pyo3(name = $sniff_name_lax)]
240            #[pyo3(text_signature = "(infile: str, indices: tuple[int], /)")]
241            fn [< read_ $name _by_indices_lax_py >](
242                infile: PathBuf, indices: Vec<i32>
243            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
244                let result = [< $name:camel Record >]::read_file_by_indices_lax(infile, &indices).map_err(PyErr::from)?;
245                Ok((
246                    result.0.into_iter().map(|rec| rec.inner()).collect(),
247                    result.1,
248                ))
249            }
250
251            #[doc = "Reads a `" $name:upper "` buffer, returning the `nth` record(s)." ]
252            #[pyfunction]
253            #[pyo3(name = $sniff_bytes)]
254            #[pyo3(text_signature = "(buf: bytes, indices: tuple[int], /)")]
255            fn [< read_ $name _bytes_by_indices_py >](buf: &[u8], indices: Vec<i32>) -> PyResult<Vec<IndexMap<String, DmapField>>> {
256                Ok([< $name:camel Record >]::read_nth_records(buf, &indices)
257                    .map_err(PyErr::from)?
258                    .into_iter()
259                    .map(|rec| rec.inner())
260                    .collect()
261                )
262            }
263
264            #[doc = "Reads a `" $name:upper "` buffer, returning the `nth` record(s) and the byte index where record corruption starts, if applicable." ]
265            #[pyfunction]
266            #[pyo3(name = $sniff_bytes_lax)]
267            #[pyo3(text_signature = "(buf: bytes, indices: tuple[int], /)")]
268            fn [< read_ $name _bytes_by_indices_lax_py >](
269                buf: &[u8], indices: Vec<i32>
270            ) -> PyResult<(Vec<IndexMap<String, DmapField>>, Option<usize>)> {
271                let result = [< $name:camel Record >]::read_nth_records_lax(buf, &indices).map_err(PyErr::from)?;
272                Ok((
273                    result.0.into_iter().map(|rec| rec.inner()).collect(),
274                    result.1,
275                ))
276            }
277
278            #[doc = "Reads a `" $name:upper "` file, returning a list of dictionaries containing the only the metadata fields." ]
279            #[pyfunction]
280            #[pyo3(name = $metadata_name)]
281            #[pyo3(text_signature = "(infile: str, /)")]
282            fn [< read_ $name _metadata_py >](infile: PathBuf) -> PyResult<Vec<IndexMap<String, DmapField>>> {
283                Ok([< $name:camel Record >]::read_file_metadata(&infile)
284                    .map_err(PyErr::from)?
285                )
286            }
287
288            #[doc = "Reads a `" $name:upper "` file, returning the `nth` records' metadata fields." ]
289            #[pyfunction]
290            #[pyo3(name = $sniff_metadata)]
291            #[pyo3(text_signature = "(infile: str, indices: tuple[int], /)")]
292            fn [< read_ $name _metadata_by_indices_py >](infile: PathBuf, indices: Vec<i32>) -> PyResult<Vec<IndexMap<String, DmapField>>> {
293                Ok([< $name:camel Record >]::read_file_metadata_by_indices(&infile, &indices)
294                    .map_err(PyErr::from)?
295                )
296            }
297        }
298    }
299}
300
301read_py!(
302    iqdat,
303    "read_iqdat",
304    "read_iqdat_lax",
305    "read_iqdat_bytes",
306    "read_iqdat_bytes_lax",
307    "read_iqdat_by_indices",
308    "read_iqdat_by_indices_lax",
309    "read_iqdat_by_indices_bytes",
310    "read_iqdat_by_indices_bytes_lax",
311    "read_iqdat_metadata",
312    "read_iqdat_metadata_by_indices"
313);
314read_py!(
315    rawacf,
316    "read_rawacf",
317    "read_rawacf_lax",
318    "read_rawacf_bytes",
319    "read_rawacf_bytes_lax",
320    "read_rawacf_by_indices",
321    "read_rawacf_by_indices_lax",
322    "read_rawacf_by_indices_bytes",
323    "read_rawacf_by_indices_bytes_lax",
324    "read_rawacf_metadata",
325    "read_rawacf_metadata_by_indices"
326);
327read_py!(
328    fitacf,
329    "read_fitacf",
330    "read_fitacf_lax",
331    "read_fitacf_bytes",
332    "read_fitacf_bytes_lax",
333    "read_fitacf_by_indices",
334    "read_fitacf_by_indices_lax",
335    "read_fitacf_by_indices_bytes",
336    "read_fitacf_by_indices_bytes_lax",
337    "read_fitacf_metadata",
338    "read_fitacf_metadata_by_indices"
339);
340read_py!(
341    grid,
342    "read_grid",
343    "read_grid_lax",
344    "read_grid_bytes",
345    "read_grid_bytes_lax",
346    "read_grid_by_indices",
347    "read_grid_by_indices_lax",
348    "read_grid_by_indices_bytes",
349    "read_grid_by_indices_bytes_lax",
350    "read_grid_metadata",
351    "read_grid_metadata_by_indices"
352);
353read_py!(
354    map,
355    "read_map",
356    "read_map_lax",
357    "read_map_bytes",
358    "read_map_bytes_lax",
359    "read_map_by_indices",
360    "read_map_by_indices_lax",
361    "read_map_by_indices_bytes",
362    "read_map_by_indices_bytes_lax",
363    "read_map_metadata",
364    "read_map_metadata_by_indices"
365);
366read_py!(
367    snd,
368    "read_snd",
369    "read_snd_lax",
370    "read_snd_bytes",
371    "read_snd_bytes_lax",
372    "read_snd_by_indices",
373    "read_snd_by_indices_lax",
374    "read_snd_by_indices_bytes",
375    "read_snd_by_indices_bytes_lax",
376    "read_snd_metadata",
377    "read_snd_metadata_by_indices"
378);
379read_py!(
380    dmap,
381    "read_dmap",
382    "read_dmap_lax",
383    "read_dmap_bytes",
384    "read_dmap_bytes_lax",
385    "read_dmap_by_indices",
386    "read_dmap_by_indices_lax",
387    "read_dmap_by_indices_bytes",
388    "read_dmap_by_indices_bytes_lax",
389    "read_dmap_metadata",
390    "read_dmap_metadata_by_indices"
391);
392
393/// Checks that a list of dictionaries contains DMAP records, then appends to outfile.
394///
395/// **NOTE:** No type checking is done, so the fields may not be written as the expected
396/// DMAP type, e.g. `stid` might be written one byte instead of two as this function
397/// does not know that typically `stid` is two bytes.
398#[pyfunction]
399#[pyo3(name = "write_dmap")]
400#[pyo3(signature = (recs, outfile, /, bz2))]
401#[pyo3(text_signature = "(recs: list[dict], outfile: str, /, bz2: bool = False)")]
402fn write_dmap_py(
403    recs: Vec<IndexMap<String, DmapField>>,
404    outfile: PathBuf,
405    bz2: bool,
406) -> PyResult<()> {
407    try_write_dmap(recs, &outfile, bz2).map_err(PyErr::from)
408}
409
410/// Checks that a list of dictionaries contains valid DMAP records, then converts them to bytes.
411/// Returns `list[bytes]`, one entry per record.
412///
413/// **NOTE:** No type checking is done, so the fields may not be written as the expected
414/// DMAP type, e.g. `stid` might be written one byte instead of two as this function
415/// does not know that typically `stid` is two bytes.
416#[pyfunction]
417#[pyo3(name = "write_dmap_bytes")]
418#[pyo3(signature = (recs, /, bz2))]
419#[pyo3(text_signature = "(recs: list[dict], /, bz2: bool = False)")]
420fn write_dmap_bytes_py(
421    py: Python,
422    recs: Vec<IndexMap<String, DmapField>>,
423    bz2: bool,
424) -> PyResult<Py<PyAny>> {
425    let mut bytes = DmapRecord::try_into_bytes(recs).map_err(PyErr::from)?;
426    if bz2 {
427        bytes = compression::compress_bz2(&bytes).map_err(PyErr::from)?;
428    }
429    Ok(PyBytes::new(py, &bytes).into())
430}
431
432/// Generates functions exposed to the Python API for writing specific file types.
433macro_rules! write_py {
434    ($name:ident, $fn_name:literal, $bytes_name:literal) => {
435        paste! {
436            #[doc = "Checks that a list of dictionaries contains valid `" $name:upper "` records, then appends to outfile." ]
437            #[pyfunction]
438            #[pyo3(name = $fn_name)]
439            #[pyo3(signature = (recs, outfile, /, bz2))]
440            #[pyo3(text_signature = "(recs: list[dict], outfile: str, /, bz2: bool = False)")]
441            fn [< write_ $name _py >](recs: Vec<IndexMap<String, DmapField>>, outfile: PathBuf, bz2: bool) -> PyResult<()> {
442                [< try_write_ $name >](recs, &outfile, bz2).map_err(PyErr::from)
443            }
444
445            #[doc = "Checks that a list of dictionaries contains valid `" $name:upper "` records, then converts them to bytes." ]
446            #[doc = "Returns `list[bytes]`, one entry per record." ]
447            #[pyfunction]
448            #[pyo3(name = $bytes_name)]
449            #[pyo3(signature = (recs, /, bz2))]
450            #[pyo3(text_signature = "(recs: list[dict], /, bz2: bool = False)")]
451            fn [< write_ $name _bytes_py >](py: Python, recs: Vec<IndexMap<String, DmapField>>, bz2: bool) -> PyResult<Py<PyAny>> {
452                let mut bytes = [< $name:camel Record >]::try_into_bytes(recs).map_err(PyErr::from)?;
453                if bz2 {
454                    bytes = compression::compress_bz2(&bytes).map_err(PyErr::from)?;
455                }
456                Ok(PyBytes::new(py, &bytes).into())
457            }
458        }
459    }
460}
461
462// **NOTE** dmap type not included in this list, since it has a more descriptive docstring.
463write_py!(iqdat, "write_iqdat", "write_iqdat_bytes");
464write_py!(rawacf, "write_rawacf", "write_rawacf_bytes");
465write_py!(fitacf, "write_fitacf", "write_fitacf_bytes");
466write_py!(grid, "write_grid", "write_grid_bytes");
467write_py!(map, "write_map", "write_map_bytes");
468write_py!(snd, "write_snd", "write_snd_bytes");
469
470/// Functions for SuperDARN DMAP file format I/O.
471#[pymodule]
472fn dmap_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
473    // Strict read functions
474    m.add_function(wrap_pyfunction!(read_dmap_py, m)?)?;
475    m.add_function(wrap_pyfunction!(read_iqdat_py, m)?)?;
476    m.add_function(wrap_pyfunction!(read_rawacf_py, m)?)?;
477    m.add_function(wrap_pyfunction!(read_fitacf_py, m)?)?;
478    m.add_function(wrap_pyfunction!(read_snd_py, m)?)?;
479    m.add_function(wrap_pyfunction!(read_grid_py, m)?)?;
480    m.add_function(wrap_pyfunction!(read_map_py, m)?)?;
481
482    // Lax read functions
483    m.add_function(wrap_pyfunction!(read_dmap_lax_py, m)?)?;
484    m.add_function(wrap_pyfunction!(read_iqdat_lax_py, m)?)?;
485    m.add_function(wrap_pyfunction!(read_rawacf_lax_py, m)?)?;
486    m.add_function(wrap_pyfunction!(read_fitacf_lax_py, m)?)?;
487    m.add_function(wrap_pyfunction!(read_snd_lax_py, m)?)?;
488    m.add_function(wrap_pyfunction!(read_grid_lax_py, m)?)?;
489    m.add_function(wrap_pyfunction!(read_map_lax_py, m)?)?;
490
491    // Read functions from byte buffer
492    m.add_function(wrap_pyfunction!(read_dmap_bytes_py, m)?)?;
493    m.add_function(wrap_pyfunction!(read_iqdat_bytes_py, m)?)?;
494    m.add_function(wrap_pyfunction!(read_rawacf_bytes_py, m)?)?;
495    m.add_function(wrap_pyfunction!(read_fitacf_bytes_py, m)?)?;
496    m.add_function(wrap_pyfunction!(read_snd_bytes_py, m)?)?;
497    m.add_function(wrap_pyfunction!(read_grid_bytes_py, m)?)?;
498    m.add_function(wrap_pyfunction!(read_map_bytes_py, m)?)?;
499
500    // Read select records from byte buffer
501    m.add_function(wrap_pyfunction!(read_dmap_bytes_by_indices_py, m)?)?;
502    m.add_function(wrap_pyfunction!(read_iqdat_bytes_by_indices_py, m)?)?;
503    m.add_function(wrap_pyfunction!(read_rawacf_bytes_by_indices_py, m)?)?;
504    m.add_function(wrap_pyfunction!(read_fitacf_bytes_by_indices_py, m)?)?;
505    m.add_function(wrap_pyfunction!(read_snd_bytes_by_indices_py, m)?)?;
506    m.add_function(wrap_pyfunction!(read_grid_bytes_by_indices_py, m)?)?;
507    m.add_function(wrap_pyfunction!(read_map_bytes_by_indices_py, m)?)?;
508
509    // Read select records from byte buffer, without raising error
510    m.add_function(wrap_pyfunction!(read_dmap_bytes_by_indices_lax_py, m)?)?;
511    m.add_function(wrap_pyfunction!(read_iqdat_bytes_by_indices_lax_py, m)?)?;
512    m.add_function(wrap_pyfunction!(read_rawacf_bytes_by_indices_lax_py, m)?)?;
513    m.add_function(wrap_pyfunction!(read_fitacf_bytes_by_indices_lax_py, m)?)?;
514    m.add_function(wrap_pyfunction!(read_snd_bytes_by_indices_lax_py, m)?)?;
515    m.add_function(wrap_pyfunction!(read_grid_bytes_by_indices_lax_py, m)?)?;
516    m.add_function(wrap_pyfunction!(read_map_bytes_by_indices_lax_py, m)?)?;
517
518    // Lax read functions from byte buffer
519    m.add_function(wrap_pyfunction!(read_dmap_bytes_lax_py, m)?)?;
520    m.add_function(wrap_pyfunction!(read_iqdat_bytes_lax_py, m)?)?;
521    m.add_function(wrap_pyfunction!(read_rawacf_bytes_lax_py, m)?)?;
522    m.add_function(wrap_pyfunction!(read_fitacf_bytes_lax_py, m)?)?;
523    m.add_function(wrap_pyfunction!(read_snd_bytes_lax_py, m)?)?;
524    m.add_function(wrap_pyfunction!(read_grid_bytes_lax_py, m)?)?;
525    m.add_function(wrap_pyfunction!(read_map_bytes_lax_py, m)?)?;
526
527    // Write functions
528    m.add_function(wrap_pyfunction!(write_dmap_py, m)?)?;
529    m.add_function(wrap_pyfunction!(write_iqdat_py, m)?)?;
530    m.add_function(wrap_pyfunction!(write_rawacf_py, m)?)?;
531    m.add_function(wrap_pyfunction!(write_fitacf_py, m)?)?;
532    m.add_function(wrap_pyfunction!(write_grid_py, m)?)?;
533    m.add_function(wrap_pyfunction!(write_map_py, m)?)?;
534    m.add_function(wrap_pyfunction!(write_snd_py, m)?)?;
535
536    // Convert records to bytes
537    m.add_function(wrap_pyfunction!(write_dmap_bytes_py, m)?)?;
538    m.add_function(wrap_pyfunction!(write_iqdat_bytes_py, m)?)?;
539    m.add_function(wrap_pyfunction!(write_rawacf_bytes_py, m)?)?;
540    m.add_function(wrap_pyfunction!(write_fitacf_bytes_py, m)?)?;
541    m.add_function(wrap_pyfunction!(write_snd_bytes_py, m)?)?;
542    m.add_function(wrap_pyfunction!(write_grid_bytes_py, m)?)?;
543    m.add_function(wrap_pyfunction!(write_map_bytes_py, m)?)?;
544
545    // Sniff records by index
546    m.add_function(wrap_pyfunction!(read_dmap_by_indices_py, m)?)?;
547    m.add_function(wrap_pyfunction!(read_iqdat_by_indices_py, m)?)?;
548    m.add_function(wrap_pyfunction!(read_rawacf_by_indices_py, m)?)?;
549    m.add_function(wrap_pyfunction!(read_fitacf_by_indices_py, m)?)?;
550    m.add_function(wrap_pyfunction!(read_snd_by_indices_py, m)?)?;
551    m.add_function(wrap_pyfunction!(read_grid_by_indices_py, m)?)?;
552    m.add_function(wrap_pyfunction!(read_map_by_indices_py, m)?)?;
553
554    // Sniff records by index, but report corrupt records
555    m.add_function(wrap_pyfunction!(read_dmap_by_indices_lax_py, m)?)?;
556    m.add_function(wrap_pyfunction!(read_iqdat_by_indices_lax_py, m)?)?;
557    m.add_function(wrap_pyfunction!(read_rawacf_by_indices_lax_py, m)?)?;
558    m.add_function(wrap_pyfunction!(read_fitacf_by_indices_lax_py, m)?)?;
559    m.add_function(wrap_pyfunction!(read_snd_by_indices_lax_py, m)?)?;
560    m.add_function(wrap_pyfunction!(read_grid_by_indices_lax_py, m)?)?;
561    m.add_function(wrap_pyfunction!(read_map_by_indices_lax_py, m)?)?;
562
563    // Read only the metadata from files
564    m.add_function(wrap_pyfunction!(read_dmap_metadata_py, m)?)?;
565    m.add_function(wrap_pyfunction!(read_iqdat_metadata_py, m)?)?;
566    m.add_function(wrap_pyfunction!(read_rawacf_metadata_py, m)?)?;
567    m.add_function(wrap_pyfunction!(read_fitacf_metadata_py, m)?)?;
568    m.add_function(wrap_pyfunction!(read_snd_metadata_py, m)?)?;
569    m.add_function(wrap_pyfunction!(read_grid_metadata_py, m)?)?;
570    m.add_function(wrap_pyfunction!(read_map_metadata_py, m)?)?;
571
572    // Read only the metadata of select records from files
573    m.add_function(wrap_pyfunction!(read_dmap_metadata_by_indices_py, m)?)?;
574    m.add_function(wrap_pyfunction!(read_iqdat_metadata_by_indices_py, m)?)?;
575    m.add_function(wrap_pyfunction!(read_rawacf_metadata_by_indices_py, m)?)?;
576    m.add_function(wrap_pyfunction!(read_fitacf_metadata_by_indices_py, m)?)?;
577    m.add_function(wrap_pyfunction!(read_snd_metadata_by_indices_py, m)?)?;
578    m.add_function(wrap_pyfunction!(read_grid_metadata_by_indices_py, m)?)?;
579    m.add_function(wrap_pyfunction!(read_map_metadata_by_indices_py, m)?)?;
580
581    Ok(())
582}