diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 000000000..b76a01e04 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,119 @@ +name: python + +on: + push: + pull_request: + release: + types: [created] + +jobs: + macos: + runs-on: macos-latest + continue-on-error: true + strategy: + matrix: + py_ver: ['3.8', '3.9', '3.10'] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py_ver }} + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin + - name: Build + uses: PyO3/maturin-action@v1 + with: + target: x86_64-apple-darwin + args: --release -i python${{ matrix.py_ver }} --all-features -m rinex/Cargo.toml --out dist --sdist + - name: Test wheel + run: | + pip install rinex --no-index --find-links dist --force-reinstall + python -c "import rinex" + - name: Prepare for upload + run: tar czvf rinex.tar.gz dist/*.whl + - name: Github Release + uses: svenstaro/upload-release-action@v2 + if: "startsWith(github.ref, 'refs/tags/')" + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: rinex-python${{ matrix.pyver }}-apple-darwin.tar.gz + file: rinex.tar.gz + tag: ${{ github.ref }} + + windows: + runs-on: windows-latest + continue-on-error: true + strategy: + matrix: + target: [x64, x86] + py_ver: ['3.8', '3.9', '3.10'] + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py_ver }} + architecture: ${{ matrix.target }} + - name: Build + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --all-features -m rinex/Cargo.toml --out dist + - name: Test wheel + run: | + pip install rinex --no-index --find-links dist --force-reinstall + python -c "import rinex" + - name: Prepare for upload + run: | + Compress-Archive dist/*.whl rinex.zip + dir + - name: Github Release + uses: svenstaro/upload-release-action@v2 + if: "startsWith(github.ref, 'refs/tags/')" + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: rinex-python${{ matrix.pyver }}-windows-${{ matrix.target }}.zip + file: rinex.zip + tag: ${{ github.ref }} + + linux: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + target: [x64] + py_ver: ['3.8', '3.9', '3.10'] + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py_ver }} + architecture: ${{ matrix.target }} + - name: Build + uses: PyO3/maturin-action@main + with: + target: x64 + manylinux: auto + args: --release -i python${{ matrix.py_ver }} --all-features -m rinex/Cargo.toml -o dist + - name: Build + run: | + pip3 install rinex --no-index --find-links dist --force-reinstall + python -c "import rinex" + - name: Test wheel + run: | + pip install rinex --no-index --find-links dist --force-reinstall + python -c "import rinex" + - name: Prepare for upload + run: | + tar czvf rinex.tar.gz dist/*.whl + ls -lah rinex.tar.gz + - name: Github Release + uses: svenstaro/upload-release-action@v2 + if: "startsWith(github.ref, 'refs/tags/')" + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: rinex-python${{ matrix.pyver }}-linux-${{ matrix.target }}.tar.gz + file: rinex.tar.gz + tag: ${{ github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa6aea707..c42e3d027 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,10 @@ name: release on: release: types: [created] + pull_request: + +env: + PKG_CONFIG_PATH: "/usr/lib/pkgconfig" env: PKG_CONFIG_PATH: "/usr/lib/pkgconfig" diff --git a/README.md b/README.md index b87a91f2c..85460f3de 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ RINEX Rust tool suites to parse, analyze and process `RINEX` files * [`rinex`](rinex/) is the core library + * [`rinex-cli`](rinex-cli/) is a command line application based on the core library. It can be used to process RINEX files or perform operations similar to `teqc`. The application is auto-generated for a few architectures, download it from the @@ -28,6 +29,14 @@ and [rinex](rinex/) crates. By default, all timestamps are in UTC with leap seconds correctly managed. +:warning: Years encoded on two digits in files generated prior Jan 1 2000, +get falsely offset to the 21st century. This only applies to OBS(V2) +and NAV(V2) files generated prior year 2000. + +## `RINEX` in Python + +Refer to the [python package](doc/python.md) to understand how to build the python3 wheel. + ## Supported `RINEX` types | Type | Parser | Writer | CLI | UBX | Notes | @@ -91,6 +100,9 @@ select a SBAS for a given location on Earth. * `--flate2` allow native parsing of .gz compressed RINEX files. Otherwise, user must uncompress manually the `.gz` extension first. +* `--pyo3` (experimental) +Add Python bindings via `PyO3`. To build the Python package, you must first install maturin and then build it with the pyo3 feature flag. For example, `maturin build -F pyo3`. Maturin will then build and place the resulting .whl file in `/target/wheels/`, after which you can install the package with `pip install rinex`. + ## Benchmark Test | Results diff --git a/doc/python.md b/doc/python.md new file mode 100644 index 000000000..f7b2e504c --- /dev/null +++ b/doc/python.md @@ -0,0 +1,25 @@ +Python3 wheel +============= + +Install requirements: python3 (>3.8) and maturin + +Then build the python bindings: use `maturin` to build the crate +with the `pyo3` feature + +```bash +cd rinex/ +maturin build -F pyo3 +``` + +A pip3 wheel is generated for your architecture. +Use pip3 to install the library + +```bash +pip3 install --force-reinstall rinex/target/wheels/rinex-xxx.whl +``` + +Now move on to the provided examples, run the basics with + +```bash +python3 rinex/examples/python/basic.py +``` diff --git a/rinex/Cargo.toml b/rinex/Cargo.toml index 26adb9d2f..cb96c4a18 100644 --- a/rinex/Cargo.toml +++ b/rinex/Cargo.toml @@ -15,6 +15,7 @@ rust-version = "1.61" [features] default = [] # no features by default sbas = ["geo", "wkt"] +pyo3 = ["dep:pyo3", "hifitime/python"] tests = [] # used to import shared test methods [build-dependencies] @@ -37,7 +38,8 @@ geo = { version = "0.26", optional = true } wkt = { version = "0.10.0", default-features = false, optional = true } serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] } flate2 = { version = "1.0.24", optional = true, default-features = false, features = ["zlib"] } -hifitime = { version = "3.8", features = ["serde", "std"] } +pyo3 = { version = "0.19", default-features = false, features = ["extension-module"], optional = true} +hifitime = { git = "https://github.com/nyx-space/hifitime", branch = "master", features = ["serde", "std"] } horrorshow = { version = "0.8" } statrs = "0.16" diff --git a/rinex/examples/python/basic.py b/rinex/examples/python/basic.py new file mode 100644 index 000000000..51bf0f329 --- /dev/null +++ b/rinex/examples/python/basic.py @@ -0,0 +1,36 @@ +from rinex import * + +# rinex::prelude basic examples, +# This example program depicts how you can interact with +# all the basic structures from the rust crate + +def parser_example(fp): + # parse a RINEX file + rinex = Rinex(fp) + # use header section + print("is_crinex: ", rinex.header.is_crinex()) + print("header : \n{:s}".format(str(rinex.header))) + # use record section + print(rinex.record) + +def rinex_manual_constructor(): + # Manual construction example. + # This is handy in data production contexts + header = Header.basic_obs() + print(header.is_crinex()) + +def sv_example(): + pass + +def constellation_example(): + pass + +def epoch_example(): + print("Epoch.system_now(): ", Epoch.system_now()) + +if __name__ == "__main__": + parser_example("../test_resources/OBS/V3/DUTH0630.22O") + epoch_example() + sv_example() + rinex_manual_constructor() + constellation_example() diff --git a/rinex/examples/python/observation.py b/rinex/examples/python/observation.py new file mode 100644 index 000000000..f3b1fe090 --- /dev/null +++ b/rinex/examples/python/observation.py @@ -0,0 +1,11 @@ +from rinex import * + +if __name__ == "__main__": + crinex = Crinex() + assert(crinex.version.major == 3) + assert(crinex.version.minor == 0) + + observation = ObservationData(10.0) + assert(observation.obs == 10.0) + assert(observation.snr == None) + assert(observation.lli == None) diff --git a/rinex/pyproject.toml b/rinex/pyproject.toml new file mode 100644 index 000000000..48a3f2fc3 --- /dev/null +++ b/rinex/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["maturin>=0.14"] +build-backend = "maturin" + +[project] +name = "rinex" +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "License :: OSI Approved :: MIT License", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Scientific/Engineering :: Atmospheric Science", +] + diff --git a/rinex/src/constellation/augmentation.rs b/rinex/src/constellation/augmentation.rs index 46acca6b0..c84b78e54 100644 --- a/rinex/src/constellation/augmentation.rs +++ b/rinex/src/constellation/augmentation.rs @@ -2,13 +2,17 @@ //! mainly used for high precision positioning use strum_macros::EnumString; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, EnumString)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// GNSS Augmentation systems, /// must be used based on current location +#[cfg_attr(feature = "pyo3", pyclass)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, EnumString)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Augmentation { /// Augmentation Unknown Unknown, diff --git a/rinex/src/constellation/mod.rs b/rinex/src/constellation/mod.rs index f4a55e6a3..8d6b7e58b 100644 --- a/rinex/src/constellation/mod.rs +++ b/rinex/src/constellation/mod.rs @@ -10,6 +10,9 @@ pub use augmentation::selection_helper; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[derive(Error, Clone, Debug, PartialEq)] /// Constellation parsing & identification related errors pub enum Error { @@ -47,6 +50,15 @@ pub enum Constellation { Mixed, } +#[cfg(feature = "pyo3")] +#[cfg_attr(feature = "pyo3", pyclass)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PyConstellation { + GPS, + Glonass, + Geo, +} + impl std::fmt::Display for Constellation { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str(self.to_3_letter_code()) @@ -185,6 +197,50 @@ impl std::str::FromStr for Constellation { } } +#[cfg(feature = "pyo3")] +impl IntoPy for Constellation { + fn into_py(self, py: Python<'_>) -> PyObject { + let pyc: PyConstellation = self.into(); + pyc.into_py(py) + } +} + +#[cfg(feature = "pyo3")] +impl From for PyConstellation { + fn from(c: Constellation) -> Self { + match c { + Constellation::SBAS(_) => Self::Geo, + Constellation::GPS => Self::GPS, + Constellation::Glonass => Self::Glonass, + _ => panic!("gnss not supported yet"), + } + } +} + +#[cfg(feature = "pyo3")] +impl From for Constellation { + fn from(c: PyConstellation) -> Self { + match c { + PyConstellation::GPS => Self::GPS, + PyConstellation::Glonass => Self::Glonass, + PyConstellation::Geo => Self::Geo, + } + } +} + +/*#[cfg(feature = "pyo3")] +use crate::prelude::Sv; +#[cfg(feature = "pyo3")] +impl From<&PyCell> for Constellation { + fn from(cell: &PyCell) -> Self { + if let Ok(sv) = cell.extract::() { + sv.constellation + } else { + Self::default() + } + } +}*/ + #[cfg(test)] mod tests { use super::*; diff --git a/rinex/src/epoch/flag.rs b/rinex/src/epoch/flag.rs index ce8ef61c2..65a276bc6 100644 --- a/rinex/src/epoch/flag.rs +++ b/rinex/src/epoch/flag.rs @@ -10,10 +10,14 @@ pub enum Error { UnknownFlag, } +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// `EpochFlag` validates an epoch, /// or describes possible events that occurred #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "pyo3", pyclass)] pub enum EpochFlag { /// Epoch is sane Ok, @@ -37,10 +41,11 @@ impl Default for EpochFlag { } } +#[cfg_attr(feature = "pyo3", pymethods)] impl EpochFlag { /// Returns True if self is a valid epoch - pub fn is_ok(self) -> bool { - self == Self::Ok + pub fn is_ok(&self) -> bool { + *self == Self::Ok } } diff --git a/rinex/src/ground_position.rs b/rinex/src/ground_position.rs index 772a9a6e2..87c873100 100644 --- a/rinex/src/ground_position.rs +++ b/rinex/src/ground_position.rs @@ -3,7 +3,11 @@ use map_3d::{deg2rad, ecef2geodetic, geodetic2ecef, rad2deg, Ellipsoid}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[derive(Copy, Debug, Clone, PartialEq, PartialOrd)] +#[cfg_attr(feature = "pyo3", derive(FromPyObject))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct GroundPosition(f64, f64, f64); diff --git a/rinex/src/hardware.rs b/rinex/src/hardware.rs index d3575733f..d2e5d760f 100644 --- a/rinex/src/hardware.rs +++ b/rinex/src/hardware.rs @@ -1,10 +1,14 @@ //! Hardware: receiver, antenna informations use super::prelude::Sv; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// GNSS receiver description +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Rcvr { @@ -42,6 +46,7 @@ impl std::str::FromStr for Rcvr { } /// Antenna description +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Antenna { @@ -62,6 +67,7 @@ pub struct Antenna { pub northern: Option, } +#[cfg_attr(feature = "pyo3", pymethods)] impl Antenna { /// Sets desired model pub fn with_model(&self, m: &str) -> Self { diff --git a/rinex/src/header.rs b/rinex/src/header.rs index ba0d66456..0f565705b 100644 --- a/rinex/src/header.rs +++ b/rinex/src/header.rs @@ -41,6 +41,10 @@ macro_rules! from_b_fmt_month { #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Clone, Debug, PartialEq, Eq, EnumString)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MarkerType { @@ -96,6 +100,7 @@ impl Default for MarkerType { /// Describes `RINEX` file header #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Header { /// revision for this `RINEX` @@ -1265,7 +1270,16 @@ impl Header { }, }) } + /// Adds desired constellation to Self + pub fn with_constellation(&self, c: Constellation) -> Self { + let mut s = self.clone(); + s.constellation = Some(c); + s + } +} +#[cfg_attr(feature = "pyo3", pymethods)] +impl Header { /// Returns true if self is a `Compressed RINEX` pub fn is_crinex(&self) -> bool { if let Some(obs) = &self.obs { @@ -1275,8 +1289,13 @@ impl Header { } } + fn __str__(&self) -> String { + self.to_string() + } + /// Creates a Basic Header structure /// for Mixed Constellation Navigation RINEX + #[staticmethod] pub fn basic_nav() -> Self { Self::default() .with_type(Type::NavigationData) @@ -1285,6 +1304,7 @@ impl Header { /// Creates a Basic Header structure /// for Mixed Constellation Observation RINEX + #[staticmethod] pub fn basic_obs() -> Self { Self::default() .with_type(Type::ObservationData) @@ -1293,6 +1313,7 @@ impl Header { /// Creates Basic Header structure /// for Compact RINEX with Mixed Constellation context + #[staticmethod] pub fn basic_crinex() -> Self { Self::default() .with_type(Type::ObservationData) @@ -1347,13 +1368,6 @@ impl Header { s } - /// Adds desired constellation to Self - pub fn with_constellation(&self, c: Constellation) -> Self { - let mut s = self.clone(); - s.constellation = Some(c); - s - } - /// adds comments to Self pub fn with_comments(&self, c: Vec) -> Self { let mut s = self.clone(); diff --git a/rinex/src/lib.rs b/rinex/src/lib.rs index 3e4cebbf0..8c5f29cf9 100644 --- a/rinex/src/lib.rs +++ b/rinex/src/lib.rs @@ -42,10 +42,9 @@ use reader::BufferedReader; use std::io::Write; //, Read}; pub mod writer; -use writer::BufferedWriter; - use std::collections::{BTreeMap, HashMap}; use thiserror::Error; +use writer::BufferedWriter; use hifitime::Duration; use navigation::OrbitItem; @@ -53,6 +52,12 @@ use observable::Observable; use observation::Crinex; use version::Version; +#[cfg(feature = "pyo3")] +pub mod python; + +#[cfg(feature = "pyo3")] +use python::PyMap; + // Convenient package to import, that // comprises all basic and major structures pub mod prelude { @@ -101,6 +106,9 @@ use algorithm::{Combination, Combine, Dcb, IonoDelayDetector, Mp, Smooth}; #[macro_use] extern crate horrorshow; +#[cfg(feature = "pyo3")] +use pyo3::{exceptions::PyTypeError, prelude::*, types::PyDict}; + #[cfg(feature = "serde")] #[macro_use] extern crate serde; @@ -168,6 +176,7 @@ macro_rules! hourly_session { } #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "pyo3", pyclass)] /// `Rinex` describes a `RINEX` file. /// ``` /// use rinex::prelude::*; @@ -250,16 +259,34 @@ pub enum Error { IoError(#[from] std::io::Error), } +#[cfg_attr(feature = "pyo3", pymethods)] impl Rinex { - /// Builds a new `RINEX` struct from given header & body sections - pub fn new(header: Header, record: record::Record) -> Rinex { - Rinex { - header, - record, - comments: record::Comments::new(), + #[new] + fn new(path: String) -> Self { + Self::from_file(&path).unwrap() + } + #[getter] + fn get_header(&self) -> PyResult
{ + Ok(self.header.clone()) + } + #[setter] + fn set_header(&mut self, header: Header) -> PyResult<()> { + self.replace_header(header); + Ok(()) + } + #[getter] + fn get_record(&self) -> PyResult { + if let Some(rec) = self.record.as_obs() { + Ok(observation::PyRecord::new()) + } else { + Err(PyTypeError::new_err( + "python binding not available for this type", + )) } } +} +impl Rinex { /// Returns a copy of self with given header attributes pub fn with_header(&self, header: Header) -> Self { Rinex { diff --git a/rinex/src/navigation/eopmessage.rs b/rinex/src/navigation/eopmessage.rs index 4b7ce0868..3d3db5ced 100644 --- a/rinex/src/navigation/eopmessage.rs +++ b/rinex/src/navigation/eopmessage.rs @@ -4,6 +4,9 @@ use crate::prelude::*; use std::str::FromStr; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// EopMessage Parsing error #[derive(Debug, Error)] pub enum Error { @@ -41,6 +44,7 @@ pub enum Error { /// } /// } /// ``` +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, Default, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct EopMessage { diff --git a/rinex/src/navigation/ephemeris.rs b/rinex/src/navigation/ephemeris.rs index 7113c87da..e5a6fbccd 100644 --- a/rinex/src/navigation/ephemeris.rs +++ b/rinex/src/navigation/ephemeris.rs @@ -8,6 +8,9 @@ use std::collections::HashMap; use std::str::FromStr; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// Parsing errors #[derive(Debug, Error)] pub enum Error { @@ -92,6 +95,7 @@ pub enum Error { /// } /// ``` #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct Ephemeris { /// Clock bias [s] @@ -118,6 +122,7 @@ impl Default for Ephemeris { /// Kepler parameters #[derive(Clone, Debug)] +#[cfg_attr(feature = "pyo3", pyclass)] pub struct Kepler { /// sqrt(semi major axis) [sqrt(m)] pub a: f64, @@ -144,6 +149,7 @@ impl Kepler { /// Perturbation parameters #[derive(Clone, Debug)] +#[cfg_attr(feature = "pyo3", pyclass)] pub struct Perturbations { /// Mean motion difference from computed value [semicircles.s-1] pub dn: f64, @@ -167,6 +173,7 @@ pub struct Perturbations { pub crc: f64, } +#[cfg_attr(feature = "pyo3", pymethods)] impl Ephemeris { /// Retrieve all clock biases (bias, drift, drift rate) at once pub fn clock_data(&self) -> (f64, f64, f64) { @@ -414,6 +421,9 @@ impl Ephemeris { } Some((map_3d::rad2deg(elev), azim)) } +} + +impl Ephemeris { /* * Parses ephemeris from given line iterator */ diff --git a/rinex/src/navigation/health.rs b/rinex/src/navigation/health.rs index 092e19724..9c5bd6cab 100644 --- a/rinex/src/navigation/health.rs +++ b/rinex/src/navigation/health.rs @@ -1,6 +1,10 @@ use bitflags::bitflags; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// GNSS / GPS orbit health indication +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, FromPrimitive, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum Health { @@ -36,6 +40,7 @@ impl std::fmt::UpperExp for Health { } /// IRNSS orbit health indication +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, FromPrimitive, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum IrnssHealth { @@ -59,6 +64,7 @@ impl std::fmt::UpperExp for IrnssHealth { } /// SBAS/GEO orbit health indication +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, FromPrimitive, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum GeoHealth { @@ -82,6 +88,7 @@ impl std::fmt::UpperExp for GeoHealth { } /// GLO orbit health indication +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, FromPrimitive, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum GloHealth { @@ -108,6 +115,7 @@ bitflags! { /// GAL orbit health indication #[derive(Debug, Default, Copy, Clone)] #[derive(PartialEq, PartialOrd)] + #[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct GalHealth: u8 { const E1B_DVS = 0x01; diff --git a/rinex/src/navigation/ionmessage.rs b/rinex/src/navigation/ionmessage.rs index 469442d3c..26bb91f7e 100644 --- a/rinex/src/navigation/ionmessage.rs +++ b/rinex/src/navigation/ionmessage.rs @@ -3,6 +3,9 @@ use bitflags::bitflags; use std::str::FromStr; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// Model parsing error #[derive(Debug, Error)] pub enum Error { @@ -31,6 +34,7 @@ pub enum Error { } /// Klobuchar Parameters region +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum KbRegionCode { @@ -48,6 +52,7 @@ impl Default for KbRegionCode { /// Klobuchar model payload, /// we don't know how to parse the possible extra Region Code yet +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct KbModel { @@ -129,6 +134,7 @@ impl KbModel { bitflags! { #[derive(Debug, Default, Clone, Copy)] #[derive(PartialEq, PartialOrd)] + #[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct NgRegionFlags: u16 { const REGION5 = 0x01; @@ -140,6 +146,7 @@ bitflags! { } /// Nequick-G Model payload +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, Default, Copy, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct NgModel { @@ -183,6 +190,7 @@ impl NgModel { } /// BDGIM Model payload +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct BdModel { diff --git a/rinex/src/navigation/orbits.rs b/rinex/src/navigation/orbits.rs index 22602767f..b50f307cd 100644 --- a/rinex/src/navigation/orbits.rs +++ b/rinex/src/navigation/orbits.rs @@ -11,6 +11,9 @@ use thiserror::Error; include!(concat!(env!("OUT_DIR"), "/nav_orbits.rs")); +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + bitflags! { #[derive(Default, Debug, Clone)] #[derive(PartialEq, PartialOrd)] diff --git a/rinex/src/navigation/record.rs b/rinex/src/navigation/record.rs index dc32c2026..0d6ccc1fd 100644 --- a/rinex/src/navigation/record.rs +++ b/rinex/src/navigation/record.rs @@ -33,6 +33,9 @@ fn double_exponent_digits(content: &str) -> String { lines.to_string() } +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + use crate::{ epoch, gnss_time::GnssTime, merge, merge::Merge, prelude::*, split, split::Split, sv, types::Type, version::Version, @@ -48,6 +51,7 @@ use hifitime::Duration; /// Possible Navigation Frame declinations for an epoch #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum FrameClass { Ephemeris, @@ -90,6 +94,7 @@ impl std::str::FromStr for FrameClass { /// Navigation Message Types #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MsgType { /// Legacy NAV diff --git a/rinex/src/navigation/stomessage.rs b/rinex/src/navigation/stomessage.rs index 47a4157a1..0ad0b8097 100644 --- a/rinex/src/navigation/stomessage.rs +++ b/rinex/src/navigation/stomessage.rs @@ -3,6 +3,9 @@ use hifitime::Epoch; use std::str::FromStr; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// Parsing error #[derive(Debug, Error)] pub enum Error { @@ -38,6 +41,7 @@ pub enum Error { /// } /// } /// ``` +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Debug, Clone, Default, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct StoMessage { diff --git a/rinex/src/observable.rs b/rinex/src/observable.rs index 682237bf8..1fe0508d6 100644 --- a/rinex/src/observable.rs +++ b/rinex/src/observable.rs @@ -1,6 +1,9 @@ use crate::{carrier, Carrier, Constellation}; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[derive(Error, Debug, Clone, PartialEq)] pub enum Error { #[error("unknown observable")] @@ -52,6 +55,16 @@ impl Default for Observable { } } +#[cfg(feature = "pyo3")] +impl IntoPy for Observable { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Self::Phase(code) => code.into_py(py), + _ => panic!("not yet"), + } + } +} + impl Observable { pub fn is_phase_observable(&self) -> bool { match self { diff --git a/rinex/src/observation/mod.rs b/rinex/src/observation/mod.rs index e5ce5c321..e2784628a 100644 --- a/rinex/src/observation/mod.rs +++ b/rinex/src/observation/mod.rs @@ -1,12 +1,18 @@ use super::{epoch, prelude::*, version::Version}; use std::collections::HashMap; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + pub mod record; mod snr; pub use record::{LliFlags, ObservationData, Record}; pub use snr::Snr; +#[cfg(feature = "pyo3")] +pub use record::PyRecord; + macro_rules! fmt_month { ($m: expr) => { match $m { @@ -31,6 +37,7 @@ use serde::Serialize; /// Describes `Compact RINEX` specific information #[derive(Clone, Debug, PartialEq, Eq, PartialOrd)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Crinex { /// Compression program version @@ -41,6 +48,7 @@ pub struct Crinex { pub date: Epoch, } +#[cfg_attr(feature = "pyo3", pymethods)] impl Crinex { /// Sets compression algorithm revision pub fn with_version(&self, version: Version) -> Self { @@ -60,6 +68,26 @@ impl Crinex { s.date = e; s } + #[cfg(feature = "pyo3")] + #[new] + fn new_py() -> Self { + Self::default() + } + #[cfg(feature = "pyo3")] + #[getter] + fn get_version(&self) -> Version { + self.version + } + #[cfg(feature = "pyo3")] + #[getter] + fn get_prog(&self) -> &str { + &self.prog + } + #[cfg(feature = "pyo3")] + #[getter] + fn get_date(&self) -> Epoch { + self.date + } } impl Default for Crinex { @@ -96,6 +124,7 @@ impl std::fmt::Display for Crinex { /// Describes known marker types /// Observation Record specific header fields #[derive(Debug, Clone, Default, PartialEq)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HeaderFields { /// Optional CRINEX information diff --git a/rinex/src/observation/record.rs b/rinex/src/observation/record.rs index 2c7c4a97f..e723a23dd 100644 --- a/rinex/src/observation/record.rs +++ b/rinex/src/observation/record.rs @@ -25,6 +25,9 @@ use crate::{ use super::Snr; use hifitime::Duration; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[derive(Error, Debug)] pub enum Error { #[error("failed to parse epoch")] @@ -49,6 +52,7 @@ use serde::Serialize; bitflags! { #[derive(Debug, Copy, Clone)] #[derive(PartialEq, PartialOrd)] + #[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct LliFlags: u8 { /// Current epoch is marked Ok or Unknown status @@ -64,6 +68,7 @@ bitflags! { } } +#[cfg_attr(feature = "pyo3", pyclass)] #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct ObservationData { @@ -92,7 +97,10 @@ impl std::ops::AddAssign for ObservationData { } } +#[cfg_attr(feature = "pyo3", pymethods)] impl ObservationData { + #[cfg(feature = "pyo3")] + #[new] /// Builds new ObservationData structure pub fn new(obs: f64, lli: Option, snr: Option) -> ObservationData { ObservationData { obs, lli, snr } @@ -103,7 +111,7 @@ impl ObservationData { /// + LLI must match the LliFlags::OkOrUnknown flag (strictly) /// if SSI exists: /// + SNR must match the .is_ok() criteria, refer to API - pub fn is_ok(self) -> bool { + pub fn is_ok(&self) -> bool { let lli_ok = self.lli.unwrap_or(LliFlags::OK_OR_UNKNOWN) == LliFlags::OK_OR_UNKNOWN; let snr_ok = self.snr.unwrap_or(Snr::default()).strong(); lli_ok && snr_ok @@ -137,6 +145,22 @@ impl ObservationData { pub fn pr_real_distance(&self, rcvr_offset: f64, sv_offset: f64, biases: f64) -> f64 { self.obs + 299_792_458.0_f64 * (rcvr_offset - sv_offset) + biases } + + #[cfg(feature = "pyo3")] + #[getter] + fn get_obs(&self) -> f64 { + self.obs + } + #[cfg(feature = "pyo3")] + #[getter] + fn get_lli(&self) -> Option { + self.lli + } + #[cfg(feature = "pyo3")] + #[getter] + fn get_snr(&self) -> Option { + self.snr + } } /// Observation Record content. @@ -184,6 +208,36 @@ pub type Record = BTreeMap< ), >; +#[cfg(feature = "pyo3")] +use crate::python::{PyBMap, PyMap}; + +/* + * Type used by python binding, + * since we can't directly derive on external types + */ +#[cfg(feature = "pyo3")] +pub type PyRecord = + PyBMap<(Epoch, EpochFlag), (Option, PyBMap>)>; + +/* + * Implement the casts when binding to python + */ +#[cfg(feature = "pyo3")] +impl IntoPy for PyRecord { + fn into_py(self, py: Python<'_>) -> PyObject { + self.into_py(py) + //self.extract().into_py(py) + //PyBMap::<(Epoch, EpochFlag), (Option, PyBMap>)>::extract() + } +} + +#[cfg(feature = "pyo3")] +impl From<&Record> for PyRecord { + fn from(rec: &Record) -> Self { + Self::new() + } +} + /// Returns true if given content matches a new OBSERVATION data epoch pub(crate) fn is_new_epoch(line: &str, v: Version) -> bool { if v.major < 3 { diff --git a/rinex/src/observation/snr.rs b/rinex/src/observation/snr.rs index 2b06a4145..8a36bb89b 100644 --- a/rinex/src/observation/snr.rs +++ b/rinex/src/observation/snr.rs @@ -1,5 +1,8 @@ use std::str::FromStr; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + #[derive(Debug, Clone)] pub enum Error { InvalidSnrCode, @@ -8,6 +11,7 @@ pub enum Error { /// `Snr` Signal to noise ratio description, /// is attached to some observations #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Debug)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Snr { /// Snr ~= 0 dB/Hz @@ -125,7 +129,9 @@ impl From for Snr { } } +#[cfg_attr(feature = "pyo3", pymethods)] impl Snr { + #[new] pub fn new(quality: &str) -> Self { match quality.trim() { "excellent" => Self::DbHz42_47, @@ -135,20 +141,20 @@ impl Snr { } } /// Returns true if self describes a bad signal level - pub fn bad(self) -> bool { - self <= Snr::DbHz18_23 + pub fn bad(&self) -> bool { + *self <= Snr::DbHz18_23 } /// Returns true if `self` describes a weak signal level - pub fn weak(self) -> bool { - self < Snr::DbHz30_35 + pub fn weak(&self) -> bool { + *self < Snr::DbHz30_35 } /// Returns true if `self` describes a strong signal level, defined in standard specifications - pub fn strong(self) -> bool { - self >= Snr::DbHz30_35 + pub fn strong(&self) -> bool { + *self >= Snr::DbHz30_35 } /// Returns true if `self` is a very strong signal level - pub fn excellent(self) -> bool { - self > Snr::DbHz42_47 + pub fn excellent(&self) -> bool { + *self > Snr::DbHz42_47 } } diff --git a/rinex/src/python.rs b/rinex/src/python.rs new file mode 100644 index 000000000..a370c3437 --- /dev/null +++ b/rinex/src/python.rs @@ -0,0 +1,127 @@ +use pyo3::{ + exceptions::{PyException, PyTypeError}, + prelude::*, + types::PyDict, +}; + +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; + +use crate::{ + hardware::*, + header::MarkerType, + navigation::*, + observation::{record::*, Crinex, Snr}, + prelude::*, +}; + +impl std::convert::From for PyErr { + fn from(err: Error) -> PyErr { + PyException::new_err(err.to_string()) + } +} + +/* + * Type used when casting HashMaps to Python compatible + * dictionnary + */ +pub struct PyMap(HashMap); + +impl PyMap { + pub(crate) fn new() -> Self { + Self(HashMap::::new()) + } +} + +impl From> for PyMap { + fn from(map: HashMap) -> Self { + Self(map) + } +} + +impl<'a, K, V> FromPyObject<'a> for PyMap +where + K: FromPyObject<'a> + Hash + Eq, + V: FromPyObject<'a> + Hash + Eq, +{ + fn extract(ob: &'a PyAny) -> PyResult { + if let Ok(dict) = ob.downcast::() { + Ok(PyMap( + dict.items() + .extract::>()? + .into_iter() + .collect::>(), + )) + } else { + Err(PyTypeError::new_err("dictionnary like type expected")) + } + } +} + +pub struct PyBMap(BTreeMap); + +impl PyBMap { + pub(crate) fn new() -> Self { + Self(BTreeMap::::new()) + } +} + +impl From> for PyBMap { + fn from(bmap: BTreeMap) -> Self { + Self(bmap) + } +} + +impl<'a, K, V> FromPyObject<'a> for PyBMap +where + K: FromPyObject<'a> + Hash + Ord + Eq, + V: FromPyObject<'a> + Hash + Ord + Eq, +{ + fn extract(ob: &'a PyAny) -> PyResult { + if let Ok(dict) = ob.downcast::() { + Ok(PyBMap( + dict.items() + .extract::>()? + .into_iter() + .collect::>(), + )) + } else { + Err(PyTypeError::new_err("dictionnary like type expected")) + } + } +} + +#[pymodule] +fn rinex(_py: Python, m: &PyModule) -> PyResult<()> { + /* + * TODO: follow the crate module names + * this should be the wrapped in the "prelude" module + */ + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + //m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // header + m.add_class::()?; + // rinex + m.add_class::
()?; + m.add_class::()?; + /* + * TODO: Observation module + */ + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + /* + * TODO: Navigation module + */ + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} diff --git a/rinex/src/record.rs b/rinex/src/record.rs index 5cc96c2df..743505131 100644 --- a/rinex/src/record.rs +++ b/rinex/src/record.rs @@ -232,6 +232,19 @@ impl Default for Record { } } +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + +#[cfg(feature = "pyo3")] +impl IntoPy for Record { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Self::ObsRecord(r) => py.None(), //r.into_py(py), + _ => panic!("into_py() not available for this type"), + } + } +} + #[derive(Error, Debug)] pub enum Error { #[error("record parsing not supported for type \"{0}\"")] diff --git a/rinex/src/sv.rs b/rinex/src/sv.rs index 061c13497..8d00609a2 100644 --- a/rinex/src/sv.rs +++ b/rinex/src/sv.rs @@ -5,8 +5,15 @@ use thiserror::Error; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + +#[cfg(feature = "pyo3")] +use super::constellation::PyConstellation; + /// ̀`Sv` describes a Satellite Vehicle #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Sv { /// PRN identification # for this vehicle @@ -31,6 +38,17 @@ impl Sv { } } +#[cfg_attr(feature = "pyo3", pymethods)] +impl Sv { + #[new] + pub fn new_py(constellation: PyConstellation, prn: u8) -> Self { + Self { + prn, + constellation: constellation.into(), + } + } +} + impl std::str::FromStr for Sv { type Err = Error; /// Builds an `Sv` from XYY identification code. diff --git a/rinex/src/types.rs b/rinex/src/types.rs index 4c5d5d6a1..bd8a43b0a 100644 --- a/rinex/src/types.rs +++ b/rinex/src/types.rs @@ -2,8 +2,12 @@ use super::Constellation; use thiserror::Error; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// Describes all known `RINEX` file types #[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Type { /// Describes Observation Data (OBS), diff --git a/rinex/src/version.rs b/rinex/src/version.rs index 48f58a57a..0f6f6430d 100644 --- a/rinex/src/version.rs +++ b/rinex/src/version.rs @@ -1,9 +1,13 @@ //! `RINEX` revision description +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// Current `RINEX` version supported to this day pub const SUPPORTED_VERSION: Version = Version { major: 4, minor: 0 }; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "pyo3", pyclass)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Version { pub major: u8, @@ -42,11 +46,23 @@ impl std::str::FromStr for Version { } } +#[cfg_attr(feature = "pyo3", pymethods)] impl Version { /// Builds a new `Version` object + #[new] pub fn new(major: u8, minor: u8) -> Self { Self { major, minor } } + #[cfg(feature = "pyo3")] + #[getter] + fn get_major(&self) -> u8 { + self.major + } + #[cfg(feature = "pyo3")] + #[getter] + fn get_minor(&self) -> u8 { + self.minor + } /// Returns true if this version is supported pub fn is_supported(&self) -> bool { if self.major < SUPPORTED_VERSION.major {