From 179513c01900181c1595a64dcc9abc312a077571 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 12 May 2024 17:30:02 +0200 Subject: [PATCH 1/9] Upgrade to rtk V0_4_5 and other improvements * fixed timescale consideration and handling, in CLK RINEX * improved and simplified interpolation interfaces * improved CLI/Help menus * improved logs Signed-off-by: Guillaume W. Bres --- README.md | 4 +- ...pst_ppp_basic.json => gpst_cpp_basic.json} | 4 +- rinex-cli/config/rtk/gpst_spp_basic.json | 2 +- rinex-cli/src/cli/graph.rs | 66 +++++-- rinex-cli/src/cli/positioning.rs | 17 +- rinex-cli/src/graph/record/ionex.rs | 35 ++-- rinex-cli/src/positioning/cggtts/mod.rs | 53 +++--- rinex-cli/src/positioning/interp/mod.rs | 27 ++- rinex-cli/src/positioning/interp/orbit.rs | 174 +++++++++++------- rinex-cli/src/positioning/interp/time.rs | 140 ++++++++------ rinex-cli/src/positioning/mod.rs | 74 ++++++-- rinex-cli/src/positioning/ppp/mod.rs | 21 +-- rinex-cli/src/positioning/ppp/post_process.rs | 56 +++--- rinex/src/clock/record.rs | 23 +-- rinex/src/record.rs | 14 +- rinex/src/tests/clock.rs | 28 +-- rinex/src/tests/mod.rs | 4 + rinex/src/tests/parsing.rs | 7 - 18 files changed, 460 insertions(+), 289 deletions(-) rename rinex-cli/config/rtk/{gpst_ppp_basic.json => gpst_cpp_basic.json} (87%) diff --git a/README.md b/README.md index 41a52a2ac..b42a07e3b 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,10 @@ RINEX formats & applications | Observation (OBS) | :heavy_check_mark:| :heavy_check_mark: | :heavy_check_mark: :chart_with_upwards_trend: | Phase, Pseudo Range, Doppler, SSI | Epoch | GNSS (any) | | CRINEX (Compressed OBS) | :heavy_check_mark:| RNX2CRX1 :heavy_check_mark: RNX2CRX3 :construction: | :heavy_check_mark: :chart_with_upwards_trend: | Phase, Pseudo Range, Doppler, SSI | Epoch | GNSS (any) | | Meteorological data (MET) | :heavy_check_mark:| :heavy_check_mark: | :heavy_check_mark: :chart_with_upwards_trend: | Meteo sensors data (Temperature, Moisture..) | Epoch | UTC | -| Clocks (CLK) | :heavy_check_mark:| :construction: | :heavy_check_mark: :chart_with_upwards_trend: | Precise SV and Reference Clock states | Epoch | UTC | +| Clocks (CLK) | :heavy_check_mark:| :construction: | :heavy_check_mark: :chart_with_upwards_trend: | Precise SV and Reference Clock states | Epoch | GNSS (any) | | Antenna (ATX) | :heavy_check_mark:| :construction: | :construction: | Precise RX/SV Antenna calibration | `antex::Antenna` | :heavy_minus_sign: | | Ionosphere Maps (IONEX) | :heavy_check_mark:| :construction: | :heavy_check_mark: :chart_with_upwards_trend: | Ionosphere Electron density | Epoch | UTC | -| DORIS RINEX | :heavy_check_mark:| :construction: | :construction: | Temperature, Moisture, Pseudo Range and Phase observations | Epoch | TAI | +| DORIS RINEX | :heavy_check_mark:| :construction: | :heavy_check_mark: | Temperature, Moisture, Pseudo Range and Phase observations | Epoch | TAI | | SINEX (SNX) | :construction: | :construction: | :heavy_minus_sign: | SINEX are special RINEX, they are managed by a dedicated [core library](sinex/) | Epoch | :question: | | Troposphere (TRO) | :construction: | :construction: | :question: | Troposphere modeling | Epoch | :question: | | Bias (BIA) | :heavy_check_mark: | :construction: | :question: | Bias estimates, like DCB.. | Epoch | :question: | diff --git a/rinex-cli/config/rtk/gpst_ppp_basic.json b/rinex-cli/config/rtk/gpst_cpp_basic.json similarity index 87% rename from rinex-cli/config/rtk/gpst_ppp_basic.json rename to rinex-cli/config/rtk/gpst_cpp_basic.json index b3c0d43e3..5f2050d8c 100644 --- a/rinex-cli/config/rtk/gpst_ppp_basic.json +++ b/rinex-cli/config/rtk/gpst_cpp_basic.json @@ -1,8 +1,8 @@ { "method": "CodePPP", "timescale": "GPST", - "interp_order": 13, - "min_sv_elevation": 1.0, + "interp_order": 17, + "min_sv_elevation": 5.0, "solver": { "filter": "LSQ", "gdop_threshold": 10.0 diff --git a/rinex-cli/config/rtk/gpst_spp_basic.json b/rinex-cli/config/rtk/gpst_spp_basic.json index a9905d09b..733678a2e 100644 --- a/rinex-cli/config/rtk/gpst_spp_basic.json +++ b/rinex-cli/config/rtk/gpst_spp_basic.json @@ -2,7 +2,7 @@ "method": "SPP", "timescale": "GPST", "interp_order": 11, - "min_sv_elevation": 10.0, + "min_sv_elevation": 5.0, "solver": { "filter": "LSQ", "gdop_threshold": 10.0 diff --git a/rinex-cli/src/cli/graph.rs b/rinex-cli/src/cli/graph.rs index b4283330e..31a916025 100644 --- a/rinex-cli/src/cli/graph.rs +++ b/rinex-cli/src/cli/graph.rs @@ -6,43 +6,77 @@ pub fn subcommand() -> Command { .long_flag("graph") .arg_required_else_help(true) .about( - "RINEX data visualization (signals, orbits..), rendered as HTML or CSV in the workspace.", + "RINEX data analysis and visualization, rendered as HTML or CSV in the workspace.", ) + .long_about("Analysis and plots (in HTML). +When Observations are present, whether they come from Observation RINEX, Meteo or DORIS RINEX, +we can export the results as CSV too. This is particularly useful to export the results of the analysis +to other tools.") .arg( Arg::new("csv") .long("csv") .action(ArgAction::SetTrue) - .help("Generate CSV files along HTML plots.") + .help("Extract Data as CSV along HTML plots. See --help.") + .long_help("This is particularly helpful if you are interested in +using our toolbox as data parser and preprocessor and inject the results to third party programs.") ) .next_help_heading( "RINEX dependent visualizations. Will only generate graphs if related dataset is present.", ) - .next_help_heading("GNSS observations (requires OBS RINEX)") + .next_help_heading("Observations rendering (OBS, Meteo, DORIS)") .arg( Arg::new("obs") .short('o') .long("obs") .action(ArgAction::SetTrue) - .help( - "Plot all observables. -When OBS RINEX is provided, this will plot raw phase, dopplers and SSI. -When METEO RINEX is provided, data from meteo sensors is plotted too.", - ), + .help("Plot all observables described in either Observation, Meteo or DORIS RINEX. See --help") + .long_help("Use this option to plot all observations. +OBS RINEX gives GNSS signals observations, but we also support Meteo RINEX and DORIS (special observation) RINEX. + +Example (1): render GNSS signals (all of them, whether it be Phase or PR) for GPS. +Use CSV for extract and export as well: + +./target/release/rinex-cli \\ + -f test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \\ + -g --obs --csv + +Example (2): render meteo sensor observations similary. + +./target/release/rinex-cli \\ + -f test_resources/MET/V3/POTS00DEU_R_20232540000_01D_05M_MM.rnx.gz \\ + -g --obs --csv + +Example (3): render DORIS observations similarly. + +./target/release/rinex-cli \\ + -f test_resources/MET/V3/POTS00DEU_R_20232540000_01D_05M_MM.rnx.gz \\ + -g --obs --csv + +Example (4): render OBS + Meteo combination at once. +RINEX-Cli allows loading OBS + Meteo in one session. +In graph mode, this means we can render both in a single run. + +./target/release/rinex-cli \\ + -f test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \\ + -f test_resources/MET/V3/POTS00DEU_R_20232540000_01D_05M_MM.rnx.gz \\ + -g --obs --csv +") + ) + .next_help_heading("GNSS signals (requires OBS and/or DORIS RINEX)") .arg( Arg::new("dcb") .long("dcb") .action(ArgAction::SetTrue) - .help("Plot Differential Code Bias. Requires OBS RINEX."), + .help("Plot Differential Code Bias."), ) .arg( Arg::new("mp") .long("mp") .action(ArgAction::SetTrue) - .help("Plot Code Multipath. Requires OBS RINEX."), + .help("Plot Code Multipath."), ) - .next_help_heading("GNSS combinations (requires OBS RINEX)") .arg( Arg::new("if") .short('i') @@ -149,4 +183,14 @@ It is the temporal equuivalent to |BRDC-SP3| requested with --sp3-residual.") .action(ArgAction::SetTrue) .help("Plot ionospheric delay per signal & SV, at latitude and longitude of signal sampling."), ) + .next_help_heading("DORIS (requires at least one DORIS file)"). + arg( + Arg::new("acorr") + .short('a') + .long("acorr") + .action(ArgAction::SetTrue) + .help("Compute and render the autocorrelation of (precise) Pseudo Range and Dopplers from the DORIS measurement, +from all contained stations. See --help") + .long_help("TODO") + ) } diff --git a/rinex-cli/src/cli/positioning.rs b/rinex-cli/src/cli/positioning.rs index ab99070e0..675b9dfc0 100644 --- a/rinex-cli/src/cli/positioning.rs +++ b/rinex-cli/src/cli/positioning.rs @@ -7,18 +7,18 @@ pub fn subcommand() -> Command { .short_flag('p') .arg_required_else_help(false) .about("Precise Positioning opmode. -Use this mode to resolve precise positions and local time from RINEX dataset. -You should provide Observations from a unique receiver.") +Use this mode to resolve Position Velocity and Time (PVT) solutions from one GNSS context.") .arg(Arg::new("cfg") .short('c') .long("cfg") .value_name("FILE") .required(false) .action(ArgAction::Append) - .help("Pass a Position Solver configuration file (JSON). + .help("Pass a Position Solver configuration file (JSON). See --help.") + .long_help(" +Use [https://github.com/georust/rinex/rinex-cli/config.rtk] as a starting point. [https://docs.rs/gnss-rtk/latest/gnss_rtk/prelude/struct.Config.html] is the structure to represent in JSON. -Refer to [https://docs.rs/gnss-rtk/latest/gnss_rtk/prelude/enum.Method.html] for solving strategies. -See [] for meaningful examples.")) +Our Wiki pages contains several examples.")) .arg(Arg::new("gpx") .long("gpx") .action(ArgAction::SetTrue) @@ -31,8 +31,11 @@ See [] for meaningful examples.")) .arg(Arg::new("cggtts") .long("cggtts") .action(ArgAction::SetTrue) - .help("Activate CGGTTS special solver. -Wrapps PVT solutions as CGGTTS file(s) for remote clock comparison (time transfer).")) + .help("Activate CGGTTS special solver. See --help.") + .long_help("In CGGTTS opmode, we're only interested in resolving the local offset to the constellation. +Navigation mode is set to [TimeOnly] and we navigate using every single vehicle in sight fitting criteria. +CGGTTS opmode is therefore more demanding as it runs the algorithm many more times than regular PPP. +The PVT solutions are then formatted as a CGGTTS file which is used to compare remote clocks to one another, from a common GNSS constellation.")) .arg(Arg::new("tracking") .long("trk") .short('t') diff --git a/rinex-cli/src/graph/record/ionex.rs b/rinex-cli/src/graph/record/ionex.rs index 74050c1e5..a2d4fab27 100644 --- a/rinex-cli/src/graph/record/ionex.rs +++ b/rinex-cli/src/graph/record/ionex.rs @@ -1,16 +1,15 @@ -//use itertools::Itertools; use crate::graph::PlotContext; -use plotly::color::NamedColor; -use plotly::common::{Marker, MarkerSymbol}; -use plotly::layout::MapboxStyle; -//use plotly::DensityMapbox; -use plotly::ScatterMapbox; +use plotly::{ + color::NamedColor, + common::{Marker, MarkerSymbol}, + layout::MapboxStyle, + {DensityMapbox, ScatterMapbox}, +}; use rinex::prelude::Rinex; pub fn plot_tec_map(data: &Rinex, _borders: ((f64, f64), (f64, f64)), plot_ctx: &mut PlotContext) { let _cmap = colorous::TURBO; - //TODO - //let hover_text: Vec = ctx.primary_data().epoch().map(|e| e.to_string()).collect(); + let hover_text: Vec = data.epoch().map(|e| e.to_string()).collect(); /* * TEC map visualization * plotly-rs has no means to animate plots at the moment @@ -41,7 +40,7 @@ pub fn plot_tec_map(data: &Rinex, _borders: ((f64, f64), (f64, f64)), plot_ctx: }, ) .collect(); - let _tec: Vec<_> = data + let tec: Vec<_> = data .tec() .filter_map( |(t, _, _, _, tec)| { @@ -75,14 +74,14 @@ pub fn plot_tec_map(data: &Rinex, _borders: ((f64, f64), (f64, f64)), plot_ctx: plot_ctx.add_trace(grid); //let map = AnimatedDensityMapbox::new(lat.clone(), lon.clone(), z) - //let map = DensityMapbox::new(lat.clone(), lon.clone(), tec.clone()) - // //.title("TEST") - // .name(epoch.to_string()) - // .opacity(0.66) - // //.hover_text_array(hover_text.clone()) - // .zauto(true) - // //.animation_frame("test") - // .zoom(3); - //plot_ctx.add_trace(map); + let map = DensityMapbox::new(lat.clone(), lon.clone(), tec.clone()) + //.title("TEST") + .name(epoch.to_string()) + .opacity(0.66) + //.hover_text_array(hover_text.clone()) + .zauto(true) + //.animation_frame("test") + .zoom(3); + plot_ctx.add_trace(map); } } diff --git a/rinex-cli/src/positioning/cggtts/mod.rs b/rinex-cli/src/positioning/cggtts/mod.rs index 04583f7e4..7f239e28d 100644 --- a/rinex-cli/src/positioning/cggtts/mod.rs +++ b/rinex-cli/src/positioning/cggtts/mod.rs @@ -10,6 +10,7 @@ use gnss::prelude::{Constellation, SV}; use rinex::{carrier::Carrier, prelude::Observable}; +use super::cast_rtk_carrier; use super::interp::TimeInterpolator; use rtk::prelude::{ @@ -163,6 +164,7 @@ where let carrier = carrier.unwrap(); let frequency = carrier.frequency(); + let rtk_carrier = cast_rtk_carrier(carrier); let mut code = Option::::None; let phase = Option::::None; @@ -170,29 +172,29 @@ where if observable.is_pseudorange_observable() { code = Some(Observation { - frequency, + carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); // attach one phase, if need be match solver.cfg.method { - Method::SPP => {}, // nothing to do + Method::SPP => {}, // nothing to do Method::CodePPP => {}, // nothing to do - //Method::PPP => { - // // try to attach phase data - // let to_match = - // Observable::from_str(&format!("L{}", &observable.to_string()[1..])).unwrap(); - // for (observable, data) in observations { - // if *observable == phase_to_match { - // phase = Some(Observation { - // frequency, - // snr: { data.snr.map(|snr| snr.into()) }, - // value: data.obs, - // }); - // } - // } - //}, + Method::PPP => { + // try to attach phase data + // let to_match = + // Observable::from_str(&format!("L{}", &observable.to_string()[1..])).unwrap(); + // for (observable, data) in observations { + // if *observable == phase_to_match { + // phase = Some(Observation { + // carrier: rtk_carrier, + // snr: { data.snr.map(|snr| snr.into()) }, + // value: data.obs, + // }); + // } + // } + }, } // try to attach doppler @@ -202,7 +204,7 @@ where for (observable, data) in observations { if *observable == doppler_to_match { doppler = Some(Observation { - frequency, + carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); @@ -234,14 +236,15 @@ where for (observable, data) in observations { if *observable == code_to_match { codes.push(Observation { - frequency: freq_to_match, - snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, + carrier: rtk_carrier, + snr: { data.snr.map(|snr| snr.into()) }, }); break; } } }, + Method::PPP => {}, //TODO }; let dopplers = match doppler { @@ -254,21 +257,15 @@ where }; let candidate = Candidate::new(*sv, *t, clock_corr, sv_eph.tgd(), codes, phases, dopplers); - match solver.resolve( - *t, - PVTSolutionType::TimeOnly, - vec![candidate], - &iono_bias, - &tropo_bias, - ) { + match solver.resolve(*t, &vec![candidate], &iono_bias, &tropo_bias) { Ok((t, pvt_solution)) => { let pvt_data = pvt_solution.sv.get(sv).unwrap(); // infaillible let azimuth = pvt_data.azimuth; let elevation = pvt_data.elevation; - let refsys = pvt_solution.dt; - let refsv = pvt_solution.dt + clock_corr.to_seconds(); + let refsys = pvt_solution.dt.to_seconds(); + let refsv = refsys + clock_corr.to_seconds(); /* * TROPO : always present diff --git a/rinex-cli/src/positioning/interp/mod.rs b/rinex-cli/src/positioning/interp/mod.rs index d20a4a8dc..7e7d4c512 100644 --- a/rinex-cli/src/positioning/interp/mod.rs +++ b/rinex-cli/src/positioning/interp/mod.rs @@ -6,22 +6,33 @@ pub use time::Interpolator as TimeInterpolator; use rinex::prelude::{Duration, Epoch}; -/// Interpolators internal buffer -pub trait Buffer { +/// Interpolators internal buffer. +/// Both interpolators, whether it be Temporal or Position, both work on 3D values represented as f64 double precision. +pub trait Buffer { /// Memory allocation fn malloc(size: usize) -> Self; /// Return current number of symbols fn len(&self) -> usize; /// Return symbol by index - fn get(&self, index: usize) -> Option<&(Epoch, T)>; + fn get(&self, index: usize) -> Option<&(Epoch, (f64, f64, f64))>; /// Clear all symbols fn clear(&mut self); /// New new symbol - fn push(&mut self, x_j: (Epoch, T)); + fn push(&mut self, x_j: (Epoch, (f64, f64, f64))); /// Returns internal symbols - fn snapshot(&self) -> &[(Epoch, T)]; + fn snapshot(&self) -> &[(Epoch, (f64, f64, f64))]; + /// Returns true if an interpolation of this order is feasible @ t + fn feasible(&self, order: usize, t: Epoch) -> bool; + /// Returns direct output in rare cases where Interpolation is not needed. + /// This avoids introduction extra bias in the measurement, due to the interpolation process. + fn direct_output(&self, t: Epoch) -> Option<&(f64, f64, f64)> { + self.snapshot() + .iter() + .filter_map(|(k, v)| if *k == t { Some(v) } else { None }) + .reduce(|k, _| k) + } /// Returns mutable internal symbols - fn snapshot_mut(&mut self) -> &mut [(Epoch, T)]; + fn snapshot_mut(&mut self) -> &mut [(Epoch, (f64, f64, f64))]; /// Returns latest interval fn last_dt(&self) -> Option<(Epoch, Duration)> { if self.len() > 1 { @@ -33,10 +44,10 @@ pub trait Buffer { } } /// Streams data in, in chronological order with gap intolerance. - fn fill(&mut self, x_j: (Epoch, T)) { + fn fill(&mut self, x_j: (Epoch, (f64, f64, f64))) { if let Some((last, dt)) = self.last_dt() { if (x_j.0 - last).to_seconds().is_sign_positive() { - // TODO: make gap tolerance more flexible + // NB Should we make gap tolerance more flexible ? if (x_j.0 - last) > dt { warn!("{} - {} gap detected - buffer reset", x_j.0, x_j.0 - last); self.clear(); diff --git a/rinex-cli/src/positioning/interp/orbit.rs b/rinex-cli/src/positioning/interp/orbit.rs index 4ac556ed0..5629663cf 100644 --- a/rinex-cli/src/positioning/interp/orbit.rs +++ b/rinex-cli/src/positioning/interp/orbit.rs @@ -14,7 +14,7 @@ struct Buffer { inner: Vec<(Epoch, (f64, f64, f64))>, } -impl BufferTrait<(f64, f64, f64)> for Buffer { +impl BufferTrait for Buffer { fn malloc(size: usize) -> Self { Self { inner: Vec::with_capacity(size), @@ -38,15 +38,25 @@ impl BufferTrait<(f64, f64, f64)> for Buffer { fn len(&self) -> usize { self.inner.len() } + fn feasible(&self, order: usize, t: Epoch) -> bool { + let before_t = self.inner.iter().filter(|(k, _)| *k < t).count(); + let after_t = self.inner.iter().filter(|(k, _)| *k > t).count(); + let size = (order + 1) / 2; // restricted to odd orders + before_t >= size && after_t >= size + } } /// Orbital state interpolator pub struct Interpolator<'a> { + // Interpolation order order: usize, + // Total counter epochs: usize, - sampling: Duration, + // Reference position apriori: AprioriPosition, + // Internal buffers buffers: HashMap, + // Data source iter: Box + 'a>, } @@ -81,40 +91,47 @@ impl<'a> Interpolator<'a> { apriori, epochs: 0, buffers: HashMap::with_capacity(128), - sampling: Duration::from_seconds(15.0 * 60.0), // TODO improve this iter: if let Some(sp3) = ctx.data.sp3() { if let Some(atx) = ctx.data.antex() { - Box::new(sp3.sv_position().filter_map(move |(t, sv, (x, y, z))| { - // TODO: need to complexify the whole interface - // to provide correct information with respect to frequency - if let Some(delta) = atx.sv_antenna_apc_offset(t, sv, Carrier::L1) { - let delta = Vector3::::new(delta.0, delta.1, delta.2); - let r_sat = Vector3::::new(x * 1.0E3, y * 1.0E3, z * 1.0E3); - let k = -r_sat - / (r_sat[0].powi(2) + r_sat[1].powi(2) + r_sat[3].powi(2)).sqrt(); + Box::new( + sp3.sv_position() + .filter_map(move |(t, sv, (x, y, z))| { + // TODO: need to complexify the whole interface + // to provide correct information with respect to frequency + if let Some(delta) = atx.sv_antenna_apc_offset(t, sv, Carrier::L1) { + let delta = Vector3::::new(delta.0, delta.1, delta.2); + let r_sat = + Vector3::::new(x * 1.0E3, y * 1.0E3, z * 1.0E3); + let k = -r_sat + / (r_sat[0].powi(2) + r_sat[1].powi(2) + r_sat[3].powi(2)) + .sqrt(); - let r_sun = sun_unit_vector(&earth_frame, &cosmic, t); - let norm = ((r_sun[0] - r_sat[0]).powi(2) - + (r_sun[1] - r_sat[1]).powi(2) - + (r_sun[2] - r_sat[2]).powi(2)) - .sqrt(); + let r_sun = sun_unit_vector(&earth_frame, &cosmic, t); + let norm = ((r_sun[0] - r_sat[0]).powi(2) + + (r_sun[1] - r_sat[1]).powi(2) + + (r_sun[2] - r_sat[2]).powi(2)) + .sqrt(); - let e = (r_sun - r_sat) / norm; - let j = Vector3::::new(k[0] * e[0], k[1] * e[1], k[2] * e[2]); - let i = Vector3::::new(j[0] * k[0], j[1] * k[1], j[2] * k[2]); - let r_dot = Vector3::::new( - (i[0] + j[0] + k[0]) * delta[0], - (i[1] + j[1] + k[1]) * delta[1], - (i[2] + j[2] + k[2]) * delta[2], - ); + let e = (r_sun - r_sat) / norm; + let j = + Vector3::::new(k[0] * e[0], k[1] * e[1], k[2] * e[2]); + let i = + Vector3::::new(j[0] * k[0], j[1] * k[1], j[2] * k[2]); + let r_dot = Vector3::::new( + (i[0] + j[0] + k[0]) * delta[0], + (i[1] + j[1] + k[1]) * delta[1], + (i[2] + j[2] + k[2]) * delta[2], + ); - let r_sat = r_sat + r_dot; - Some((t, sv, (r_sat[0], r_sat[1], r_sat[1]))) - } else { - error!("{:?} ({}) - failed to determine APC offset", t, sv); - None - } - })) + let r_sat = r_sat + r_dot; + Some((t, sv, (r_sat[0], r_sat[1], r_sat[1]))) + } else { + error!("{:?} ({}) - failed to determine APC offset", t, sv); + None + } + }) + .peekable(), + ) } else { warn!("Cannot determine exact APC coordinates without ANTEX data."); warn!("Expect tiny offsets in final results."); @@ -124,7 +141,14 @@ impl<'a> Interpolator<'a> { ) } } else { - unreachable!("sp3 data required at the moment"); + let brdc = ctx + .data + .brdc_navigation() + .expect("BRDC navigation required"); + Box::new( + brdc.sv_position() + .map(|(t, sv, (x, y, z))| (t, sv, (x * 1.0E3, y * 1.0E3, z * 1.0E3))), + ) }, } } @@ -137,7 +161,7 @@ impl<'a> Interpolator<'a> { self.buffers.insert(sv, buf); } } - // consumes N epochs completely + // Consumes N epochs completely fn consume(&mut self, total: usize) -> bool { let mut prev_t = None; let mut epochs = 0; @@ -159,53 +183,57 @@ impl<'a> Interpolator<'a> { self.epochs += epochs; false } - fn latest(&self, sv: SV) -> Option<&Epoch> { - self.buffers - .iter() - .filter_map(|(k, v)| { - if *k == sv { - let last = v.inner.iter().map(|(e, _)| e).last()?; - Some(last) - } else { - None - } - }) - .reduce(|k, _| k) + // fn latest(&self, sv: SV) -> Option<&Epoch> { + // self.buffers + // .iter() + // .filter_map(|(k, v)| { + // if *k == sv { + // let last = v.inner.iter().map(|(e, _)| e).last()?; + // Some(last) + // } else { + // None + // } + // }) + // .reduce(|k, _| k) + // } + // Returns true if interpolation is feasible @ t for SV + fn is_feasible(&self, t: Epoch, sv: SV) -> bool { + if let Some(buf) = self.buffers.get(&sv) { + buf.feasible(self.order, t) + } else { + false + } } + // Orbit interpolation @ t for SV pub fn next_at(&mut self, t: Epoch, sv: SV) -> Option { // Maintain buffer up to date, consume data if need be - let dt = Duration::from_seconds((self.order / 2) as f64 * self.sampling.to_seconds()); - loop { - if let Some(latest) = self.latest(sv) { - if *latest >= t + dt { - break; - } else if self.consume(1) { - // end of stream - break; - } - } else if self.consume(1) { + while !self.is_feasible(t, sv) { + if self.consume(1) { // end of stream - break; + return None; } } - // interp let buf = self.buffers.get_mut(&sv)?; //let len_before = buf.len(); // DEBUG - let ecef = self.apriori.ecef; + let ref_ecef = self.apriori.ecef(); + + if let Some((x, y, z)) = buf.direct_output(t) { + // No need to interpolate @ t for SV + // Preserves data precision + let el_az = + Ephemeris::elevation_azimuth((*x, *y, *z), (ref_ecef[0], ref_ecef[1], ref_ecef[2])); + return Some( + RTKInterpolationResult::from_apc_position((*x, *y, *z)) + .with_elevation_azimuth(el_az), + ); + } + let mut mid_offset = 0; let mut polynomials = (0.0_f64, 0.0_f64, 0.0_f64); let mut out = Option::::None; for (index, (buf_t, buf_v)) in buf.inner.iter().enumerate() { - if *buf_t == t { - // special case: direct output - let el_az = Ephemeris::elevation_azimuth(*buf_v, (ecef[0], ecef[1], ecef[2])); - out = Some( - RTKInterpolationResult::from_apc_position(*buf_v).with_elevation_azimuth(el_az), - ); - break; - } if *buf_t > t { break; } @@ -235,7 +263,10 @@ impl<'a> Interpolator<'a> { polynomials.2 += z_i * li; } - let el_az = Ephemeris::elevation_azimuth(polynomials, (ecef[0], ecef[1], ecef[2])); + let el_az = Ephemeris::elevation_azimuth( + polynomials, + (ref_ecef[0], ref_ecef[1], ref_ecef[2]), + ); out = Some( RTKInterpolationResult::from_apc_position(polynomials) .with_elevation_azimuth(el_az), @@ -247,8 +278,13 @@ impl<'a> Interpolator<'a> { if out.is_some() { // management: discard old samples - let t_min = t - dt - self.sampling - self.sampling; - buf.inner.retain(|b_t| b_t.0 > t_min); + // len_before = buf.len(); // DEBUG + let index_min = mid_offset - (self.order + 1) / 2 - 2; + let mut index = 0; + buf.inner.retain(|_| { + index += 1; + index > index_min + }); //let len_after = buf.len(); // DEBUG //if len_after != len_before { // DEBUG diff --git a/rinex-cli/src/positioning/interp/time.rs b/rinex-cli/src/positioning/interp/time.rs index d2dfd1353..fd654c847 100644 --- a/rinex-cli/src/positioning/interp/time.rs +++ b/rinex-cli/src/positioning/interp/time.rs @@ -4,41 +4,51 @@ use gnss_rtk::prelude::{Duration, Epoch, SV}; use std::collections::HashMap; struct Buffer { - inner: Vec<(Epoch, f64)>, + inner: Vec<(Epoch, (f64, f64, f64))>, } -impl BufferTrait for Buffer { +impl BufferTrait for Buffer { fn malloc(size: usize) -> Self { Self { inner: Vec::with_capacity(size), } } - fn push(&mut self, x_j: (Epoch, f64)) { + fn push(&mut self, x_j: (Epoch, (f64, f64, f64))) { self.inner.push(x_j); } fn clear(&mut self) { self.inner.clear(); } - fn snapshot(&self) -> &[(Epoch, f64)] { + fn snapshot(&self) -> &[(Epoch, (f64, f64, f64))] { &self.inner } - fn snapshot_mut(&mut self) -> &mut [(Epoch, f64)] { + fn snapshot_mut(&mut self) -> &mut [(Epoch, (f64, f64, f64))] { &mut self.inner } - fn get(&self, index: usize) -> Option<&(Epoch, f64)> { + fn get(&self, index: usize) -> Option<&(Epoch, (f64, f64, f64))> { self.inner.get(index) } fn len(&self) -> usize { self.inner.len() } + fn feasible(&self, _order: usize, t: Epoch) -> bool { + if self.len() < 2 { + false + } else { + let t0 = self.get(self.len() - 1).unwrap().0; // latest + let t1 = self.get(self.len() - 2).unwrap().0; // latest -1 + t1 <= t && t <= t0 + } + } } /// Orbital state interpolator pub struct Interpolator<'a> { + /// Total counter epochs: usize, - sampling: Duration, + /// Internal buffer buffers: HashMap, - iter: Box + 'a>, + iter: Box + 'a>, } impl<'a> Interpolator<'a> { @@ -52,19 +62,24 @@ impl<'a> Interpolator<'a> { Self { epochs: 0, buffers: HashMap::with_capacity(32), - // TODO improve sampling determination - sampling: if ctx.data.clock().is_some() { - Duration::from_seconds(30.0) - } else { - Duration::from_seconds(15.0 * 60.0) - }, iter: if let Some(clk) = ctx.data.clock() { + Box::new(clk.precise_sv_clock().map(|(t, sv, _, prof)| { + ( + t, + sv, + ( + prof.bias, + prof.drift.unwrap_or(0.0_f64), + prof.drift_change.unwrap_or(0.0_f64), + ), + ) + })) + } else if let Some(sp3) = ctx.data.sp3() { + // TODO: improve SP3 API and definitions Box::new( - clk.precise_sv_clock() - .map(|(t, sv, _, prof)| (t, sv, prof.bias)), + sp3.sv_clock() + .map(|(t, sv, clk)| (t, sv, (clk, 0.0_f64, 0.0_f64))), ) - } else if let Some(sp3) = ctx.data.sp3() { - Box::new(sp3.sv_clock()) } else { panic!("sp3 or clock rinex currently required"); // TODO @@ -78,7 +93,7 @@ impl<'a> Interpolator<'a> { }, } } - fn push(&mut self, t: Epoch, sv: SV, data: f64) { + fn push(&mut self, t: Epoch, sv: SV, data: (f64, f64, f64)) { if let Some(buf) = self.buffers.get_mut(&sv) { buf.push((t, data)); } else { @@ -109,42 +124,55 @@ impl<'a> Interpolator<'a> { self.epochs += epochs; false } - fn latest(&self, sv: SV) -> Option<&Epoch> { - self.buffers - .iter() - .filter_map(|(k, v)| { - if *k == sv { - let last = v.inner.iter().map(|(e, _)| e).last()?; - Some(last) - } else { - None - } - }) - .reduce(|k, _| k) + // fn latest(&self, sv: SV) -> Option<&Epoch> { + // self.buffers + // .iter() + // .filter_map(|(k, v)| { + // if *k == sv { + // let last = v.inner.iter().map(|(e, _)| e).last()?; + // Some(last) + // } else { + // None + // } + // }) + // .reduce(|k, _| k) + // } + // Returns true if interpolation is feasible @ t for SV + fn is_feasible(&self, t: Epoch, sv: SV) -> bool { + if let Some(buf) = self.buffers.get(&sv) { + buf.feasible(1, t) + } else { + false + } } + // Clock offset interpolation @ t for SV pub fn next_at(&mut self, t: Epoch, sv: SV) -> Option { + let mut dt = Option::::None; + let mut first_x = Option::::None; + // Maintain buffer up to date, consume data if need be - loop { - if let Some(latest) = self.latest(sv) { - if *latest >= t + self.sampling { - break; - } else if self.consume(1) { - // end of stream - break; - } - } else if self.consume(1) { + while !self.is_feasible(t, sv) { + if self.consume(1) { // end of stream - break; + return None; } } - let buf = self.buffers.get_mut(&sv)?; + let mut buf = self.buffers.get_mut(&sv)?; + + if let Some((y, _, _)) = buf.direct_output(t) { + // No need to interpolate @ t for SV + // Preserves data precision + first_x = Some(t); + dt = Some(Duration::from_seconds(*y)); + } + + if let Some((before_x, (before_y, _, _))) = + buf.inner.iter().filter(|(v_t, _)| *v_t <= t).last() + { + first_x = Some(*before_x); - if let Some((before_x, before_y)) = buf.inner.iter().filter(|(v_t, _)| *v_t <= t).last() { - // interpolate: if need be - let dy: Option = if *before_x == t { - Some(Duration::from_seconds(*before_y)) - } else if let Some((after_x, after_y)) = buf + if let Some((after_x, (after_y, _, _))) = buf .inner .iter() .filter(|(v_t, _)| *v_t > t) @@ -153,18 +181,14 @@ impl<'a> Interpolator<'a> { let dx = (*after_x - *before_x).to_seconds(); let mut dy = (*after_x - t).to_seconds() / dx * *before_y; dy += (t - *before_x).to_seconds() / dx * *after_y; - Some(Duration::from_seconds(dy)) - } else { - None - }; - - // management: discard old samples - if dy.is_some() { - buf.inner.retain(|b| b.0 < t - self.sampling); + dt = Some(Duration::from_seconds(dy)); } - - return dy; } - None + // Discard symbols that did not contribute (too old) + if let Some(first_x) = first_x { + buf.inner.retain(|(k, v)| *k >= first_x); + } + + dt } } diff --git a/rinex-cli/src/positioning/mod.rs b/rinex-cli/src/positioning/mod.rs index e8af1008c..c1dc52295 100644 --- a/rinex-cli/src/positioning/mod.rs +++ b/rinex-cli/src/positioning/mod.rs @@ -12,10 +12,12 @@ use cggtts::PostProcessingError as CGGTTSPostProcessingError; use clap::ArgMatches; use gnss::prelude::Constellation; // SV}; +use rinex::carrier::Carrier; use rinex::prelude::{Observable, Rinex}; use rtk::prelude::{ - AprioriPosition, BdModel, Config, Duration, Epoch, KbModel, Method, NgModel, Solver, Vector3, + AprioriPosition, BdModel, Carrier as RTKCarrier, Config, Duration, Epoch, Error as RTKError, + KbModel, Method, NgModel, PVTSolutionType, Solver, Vector3, }; use map_3d::{ecef2geodetic, rad2deg, Ellipsoid}; @@ -27,7 +29,7 @@ use interp::OrbitInterpolator; #[derive(Debug, Error)] pub enum Error { #[error("solver error")] - SolverError(#[from] rtk::Error), + SolverError(#[from] RTKError), #[error("undefined apriori position")] UndefinedAprioriPosition, #[error("ppp post processing error")] @@ -36,6 +38,21 @@ pub enum Error { CGGTTSPostProcessingError(#[from] CGGTTSPostProcessingError), } +/* + * Converts `Carrier` into RTK compatible struct + */ +pub fn cast_rtk_carrier(carrier: Carrier) -> RTKCarrier { + match carrier { + Carrier::L2 => RTKCarrier::L2, + Carrier::L5 => RTKCarrier::L5, + Carrier::L6 => RTKCarrier::L6, + Carrier::E1 => RTKCarrier::E1, + Carrier::E5 => RTKCarrier::E5, + Carrier::E6 => RTKCarrier::E6, + Carrier::L1 | _ => RTKCarrier::L1, + } +} + pub fn tropo_components(meteo: Option<&Rinex>, t: Epoch, lat_ddeg: f64) -> Option<(f64, f64)> { const MAX_LATDDEG_DELTA: f64 = 15.0; let max_dt = Duration::from_hours(24.0); @@ -153,7 +170,8 @@ pub fn ng_model(nav: &Rinex, t: Epoch) -> Option { } pub fn precise_positioning(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { - let cfg = match matches.get_one::("cfg") { + /* Load customized config script, or use defaults */ + let mut cfg = match matches.get_one::("cfg") { Some(fp) => { let content = read_to_string(fp) .unwrap_or_else(|e| panic!("failed to read configuration: {}", e)); @@ -164,20 +182,18 @@ pub fn precise_positioning(ctx: &Context, matches: &ArgMatches) -> Result<(), Er }, None => { let method = Method::default(); - let cfg = Config::preset(method); + let cfg = Config::static_preset(method); info!("Using {:?} default preset: {:#?}", method, cfg); cfg }, }; - /* - * verify requirements - */ + /* Verify requirements and print helpful comments */ let apriori_ecef = ctx.rx_ecef.ok_or(Error::UndefinedAprioriPosition)?; let apriori = Vector3::::new(apriori_ecef.0, apriori_ecef.1, apriori_ecef.2); let apriori = AprioriPosition::from_ecef(apriori); - let rx_lat_ddeg = apriori.geodetic[0]; + let rx_lat_ddeg = apriori.geodetic()[0]; assert!( ctx.data.observation().is_some(), @@ -185,13 +201,45 @@ pub fn precise_positioning(ctx: &Context, matches: &ArgMatches) -> Result<(), Er ); assert!( ctx.data.brdc_navigation().is_some(), - "Positioning required Navigation RINEX" - ); - assert!( - ctx.data.sp3().is_some(), - "High precision orbits (SP3) are unfortunately mandatory at the moment" + "Positioning requires Navigation RINEX" ); + if cfg.interp_order > 5 && ctx.data.sp3().is_none() { + error!("High interpolation orders are likely incompatible with navigation based on broadcast radio."); + warn!("It is possible that this configuration does not generate any solutions."); + info!("Consider loading high precision SP3 data to use high interpolation orders."); + } + + if let Some(obs_rinex) = ctx.data.observation() { + if let Some(obs_header) = &obs_rinex.header.obs { + if let Some(time_of_first_obs) = obs_header.time_of_first_obs { + if let Some(clk_rinex) = ctx.data.clock() { + if let Some(clk_header) = &clk_rinex.header.clock { + if let Some(time_scale) = clk_header.timescale { + if time_scale == time_of_first_obs.time_scale { + info!("Temporal PPP compliancy"); + } else { + error!("Working with different timescales in OBS/CLK RINEX is not PPP compatible and will generate tiny errors"); + warn!("Consider using OBS/CLK RINEX files expressed in the same timescale for optimal results"); + } + } else { + error!("Provided Clock RINEX is badly defined and will likely introduce errors in calculations"); + } + } else { + error!("Provided Clock RINEX is badly defined and will likely introduce errors in calculations"); + } + } + } + } + } + + /* + * CGGTTS special case + */ + if matches.get_flag("cggtts") { + cfg.sol_type = PVTSolutionType::TimeOnly; + } + let orbit = RefCell::new(OrbitInterpolator::from_ctx( ctx, cfg.interp_order, diff --git a/rinex-cli/src/positioning/ppp/mod.rs b/rinex-cli/src/positioning/ppp/mod.rs index 7f579275c..9438305f9 100644 --- a/rinex-cli/src/positioning/ppp/mod.rs +++ b/rinex-cli/src/positioning/ppp/mod.rs @@ -1,6 +1,8 @@ //! PPP solver use crate::cli::Context; -use crate::positioning::{bd_model, kb_model, ng_model, tropo_components}; +use crate::positioning::{ + bd_model, cast_rtk_carrier, interp::TimeInterpolator, kb_model, ng_model, tropo_components, +}; use std::collections::BTreeMap; use rinex::{ @@ -16,8 +18,6 @@ use rtk::prelude::{ PVTSolutionType, Solver, TroposphereBias, }; -use super::interp::TimeInterpolator; - pub fn resolve( ctx: &Context, mut solver: Solver, @@ -88,23 +88,24 @@ where for (observable, data) in observations { if let Ok(carrier) = Carrier::from_observable(sv.constellation, observable) { let frequency = carrier.frequency(); + let rtk_carrier: gnss_rtk::prelude::Carrier = cast_rtk_carrier(carrier); if observable.is_pseudorange_observable() { codes.push(Observation { - frequency, + carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); } else if observable.is_phase_observable() { let lambda = carrier.wavelength(); phases.push(Observation { - frequency, + carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, value: data.obs * lambda, }); } else if observable.is_doppler_observable() { dopplers.push(Observation { - frequency, + carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); @@ -138,13 +139,7 @@ where zwd_zdd, }; - match solver.resolve( - *t, - PVTSolutionType::PositionVelocityTime, - candidates, - &iono_bias, - &tropo_bias, - ) { + match solver.resolve(*t, &candidates, &iono_bias, &tropo_bias) { Ok((t, pvt)) => { debug!("{:?} : {:?}", t, pvt); solutions.insert(t, pvt); diff --git a/rinex-cli/src/positioning/ppp/post_process.rs b/rinex-cli/src/positioning/ppp/post_process.rs index fbbfbfe61..fd8d2f553 100644 --- a/rinex-cli/src/positioning/ppp/post_process.rs +++ b/rinex-cli/src/positioning/ppp/post_process.rs @@ -57,9 +57,9 @@ pub fn post_process( let (mut lat, mut lon) = (Vec::::new(), Vec::::new()); for result in results.values() { - let px = x + result.pos.x; - let py = y + result.pos.y; - let pz = z + result.pos.z; + let px = x + result.position.x; + let py = y + result.position.y; + let pz = z + result.position.z; let (lat_ddeg, lon_ddeg, _) = ecef2geodetic(px, py, pz, Ellipsoid::WGS84); lat.push(rad2deg(lat_ddeg)); lon.push(rad2deg(lon_ddeg)); @@ -97,9 +97,9 @@ pub fn post_process( "error", Mode::Markers, epochs.clone(), - results.values().map(|e| e.pos.x).collect::>(), - results.values().map(|e| e.pos.y).collect::>(), - results.values().map(|e| e.pos.z).collect::>(), + results.values().map(|e| e.position.x).collect::>(), + results.values().map(|e| e.position.y).collect::>(), + results.values().map(|e| e.position.z).collect::>(), ); plot_ctx.add_cartesian3d_plot( @@ -115,10 +115,9 @@ pub fn post_process( * Add Spherical mesh with radius being the * largest error */ - for error in results - .values() - .map(|pvt| (pvt.pos.x.powi(2) + pvt.pos.y.powi(2) + pvt.pos.z.powi(2)).sqrt()) - { + for error in results.values().map(|pvt| { + (pvt.position.x.powi(2) + pvt.position.y.powi(2) + pvt.position.z.powi(2)).sqrt() + }) { if error > worst_radius { worst_radius = error; } @@ -133,7 +132,7 @@ pub fn post_process( "x err", Mode::Markers, epochs.clone(), - results.values().map(|p| p.pos.x).collect::>(), + results.values().map(|p| p.position.x).collect::>(), ); plot_ctx.add_trace(trace); @@ -141,7 +140,7 @@ pub fn post_process( "y err", Mode::Markers, epochs.clone(), - results.values().map(|p| p.pos.y).collect::>(), + results.values().map(|p| p.position.y).collect::>(), ) .y_axis("y2"); plot_ctx.add_trace(trace); @@ -154,7 +153,7 @@ pub fn post_process( "z err", Mode::Markers, epochs.clone(), - results.values().map(|p| p.pos.z).collect::>(), + results.values().map(|p| p.position.z).collect::>(), ); plot_ctx.add_trace(trace); @@ -170,7 +169,7 @@ pub fn post_process( "velocity (x)", Mode::Markers, epochs.clone(), - results.values().map(|p| p.vel.x).collect::>(), + results.values().map(|p| p.velocity.x).collect::>(), ); plot_ctx.add_trace(trace); @@ -178,7 +177,7 @@ pub fn post_process( "velocity (y)", Mode::Markers, epochs.clone(), - results.values().map(|p| p.vel.y).collect::>(), + results.values().map(|p| p.velocity.y).collect::>(), ) .y_axis("y2"); plot_ctx.add_trace(trace); @@ -188,7 +187,7 @@ pub fn post_process( "velocity (z)", Mode::Markers, epochs.clone(), - results.values().map(|p| p.vel.z).collect::>(), + results.values().map(|p| p.velocity.z).collect::>(), ); plot_ctx.add_trace(trace); @@ -230,7 +229,10 @@ pub fn post_process( "dt", Mode::Markers, epochs.clone(), - results.values().map(|e| e.dt).collect::>(), + results + .values() + .map(|e| e.dt.to_seconds()) + .collect::>(), ); plot_ctx.add_trace(trace); @@ -266,7 +268,11 @@ pub fn post_process( )?; for (epoch, solution) in results { - let (px, py, pz) = (x + solution.pos.x, y + solution.pos.y, z + solution.pos.z); + let (px, py, pz) = ( + x + solution.position.x, + y + solution.position.y, + z + solution.position.z, + ); let (lat, lon, alt) = map_3d::ecef2geodetic(px, py, pz, Ellipsoid::WGS84); let (hdop, vdop, tdop) = ( solution.hdop(lat_ddeg, lon_ddeg), @@ -277,18 +283,18 @@ pub fn post_process( fd, "{:?}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}", epoch, - solution.pos.x, - solution.pos.y, - solution.pos.z, + solution.position.x, + solution.position.y, + solution.position.z, px, py, pz, - solution.vel.x, - solution.vel.y, - solution.vel.z, + solution.velocity.x, + solution.velocity.y, + solution.velocity.z, hdop, vdop, - solution.dt, + solution.dt.to_seconds(), tdop )?; if matches.get_flag("gpx") { diff --git a/rinex/src/clock/record.rs b/rinex/src/clock/record.rs index adc9ea37d..ad73f6f2d 100644 --- a/rinex/src/clock/record.rs +++ b/rinex/src/clock/record.rs @@ -155,6 +155,7 @@ pub(crate) fn is_new_epoch(line: &str) -> bool { pub(crate) fn parse_epoch( version: Version, content: &str, + ts: TimeScale, ) -> Result<(Epoch, ClockKey, ClockProfile), Error> { let mut lines = content.lines(); let line = lines.next().unwrap(); @@ -194,7 +195,7 @@ pub(crate) fn parse_epoch( const OFFSET: usize = "yyyy mm dd hh mm sssssssssss".len(); let (epoch, rem) = rem.split_at(OFFSET); - let epoch = epoch::parse_utc(epoch.trim())?; + let epoch = epoch::parse_in_timescale(epoch.trim(), ts)?; // nb of data fields let (_n, rem) = rem.split_at(4); @@ -486,7 +487,7 @@ mod test { for (descriptor, epoch, key, profile) in [ ( "AS R20 2019 01 08 00 03 30.000000 1 -0.364887538519E-03", - Epoch::from_str("2019-01-08T00:03:30 UTC").unwrap(), + Epoch::from_str("2019-01-08T00:03:30 GPST").unwrap(), ClockKey { clock_type: ClockType::SV(SV::from_str("R20").unwrap()), profile_type: ClockProfileType::AS, @@ -502,7 +503,7 @@ mod test { ), ( "AS R18 2019 01 08 10 00 0.000000 2 0.294804625338E-04 0.835484069663E-11", - Epoch::from_str("2019-01-08T10:00:00 UTC").unwrap(), + Epoch::from_str("2019-01-08T10:00:00 GPST").unwrap(), ClockKey { clock_type: ClockType::SV(SV::from_str("R18").unwrap()), profile_type: ClockProfileType::AS, @@ -518,7 +519,7 @@ mod test { ), ( "AR PIE1 2019 01 08 00 04 0.000000 1 -0.434275035628E-03", - Epoch::from_str("2019-01-08T00:04:00 UTC").unwrap(), + Epoch::from_str("2019-01-08T00:04:00 GPST").unwrap(), ClockKey { clock_type: ClockType::Station("PIE1".to_string()), profile_type: ClockProfileType::AR, @@ -534,7 +535,7 @@ mod test { ), ( "AR IMPZ 2019 01 08 00 00 0.000000 2 -0.331415119107E-07 0.350626190546E-10", - Epoch::from_str("2019-01-08T00:00:00 UTC").unwrap(), + Epoch::from_str("2019-01-08T00:00:00 GPST").unwrap(), ClockKey { clock_type: ClockType::Station("IMPZ".to_string()), profile_type: ClockProfileType::AR, @@ -550,7 +551,7 @@ mod test { ), ] { let (parsed_e, parsed_k, parsed_prof) = - parse_epoch(Version { minor: 0, major: 2 }, descriptor) + parse_epoch(Version { minor: 0, major: 2 }, descriptor, TimeScale::GPST) .unwrap_or_else(|_| panic!("failed to parse \"{}\"", descriptor)); assert_eq!(parsed_e, epoch, "parsed wrong epoch"); @@ -564,7 +565,7 @@ mod test { ( "AR AREQ 1994 07 14 20 59 0.000000 6 -0.123456789012E+00 -0.123456789012E+01 -0.123456789012E+02 -0.123456789012E+03 -0.123456789012E+04 -0.123456789012E+05", - Epoch::from_str("1994-07-14T20:59:00 UTC").unwrap(), + Epoch::from_str("1994-07-14T20:59:00 GPST").unwrap(), ClockKey { clock_type: ClockType::Station("AREQ".to_string()), profile_type: ClockProfileType::AR, @@ -580,7 +581,7 @@ mod test { ), ( "AS G16 1994 07 14 20 59 0.000000 2 -0.123456789012E+00 -0.123456789012E+01", - Epoch::from_str("1994-07-14T20:59:00 UTC").unwrap(), + Epoch::from_str("1994-07-14T20:59:00 GPST").unwrap(), ClockKey { clock_type: ClockType::SV(SV::from_str("G16").unwrap()), profile_type: ClockProfileType::AS, @@ -596,7 +597,7 @@ mod test { ), ( "CR USNO 1994 07 14 20 59 0.000000 2 -0.123456789012E+00 -0.123456789012E+01", - Epoch::from_str("1994-07-14T20:59:00 UTC").unwrap(), + Epoch::from_str("1994-07-14T20:59:00 GPST").unwrap(), ClockKey { clock_type: ClockType::Station("USNO".to_string()), profile_type: ClockProfileType::CR, @@ -613,7 +614,7 @@ mod test { ( "DR USNO 1994 07 14 20 59 0.000000 2 -0.123456789012E+00 -0.123456789012E+01 -0.123456789012E-03 -0.123456789012E-04", - Epoch::from_str("1994-07-14T20:59:00 UTC").unwrap(), + Epoch::from_str("1994-07-14T20:59:00 GPST").unwrap(), ClockKey { clock_type: ClockType::Station("USNO".to_string()), profile_type: ClockProfileType::DR, @@ -629,7 +630,7 @@ mod test { ), ] { let (parsed_e, parsed_k, parsed_prof) = - parse_epoch(Version { minor: 0, major: 2 }, descriptor) + parse_epoch(Version { minor: 0, major: 2 }, descriptor, TimeScale::GPST) .unwrap_or_else(|_| panic!("failed to parse \"{}\"", descriptor)); assert_eq!(parsed_e, epoch, "parsed wrong epoch"); diff --git a/rinex/src/record.rs b/rinex/src/record.rs index 2189b0088..d22ffd063 100644 --- a/rinex/src/record.rs +++ b/rinex/src/record.rs @@ -328,6 +328,16 @@ pub fn parse_record( }, } } + // CLOCK case + // timescale is defined in header. If not + // - This RINEX is corrupt + // - and we falsely consider GPST and introduce errors + let mut clk_ts = TimeScale::default(); + if let Some(clk) = &header.clock { + if let Some(ts) = clk.timescale { + clk_ts = ts; + } + } // IONEX case // Default map type is TEC, it will come with identified Epoch // but others may exist: @@ -452,7 +462,7 @@ pub fn parse_record( }, Type::ClockData => { if let Ok((epoch, key, profile)) = - clock::record::parse_epoch(header.version, &epoch_content) + clock::record::parse_epoch(header.version, &epoch_content, clk_ts) { if let Some(e) = clk_rec.get_mut(&epoch) { e.insert(key, profile); @@ -556,7 +566,7 @@ pub fn parse_record( }, Type::ClockData => { if let Ok((epoch, key, profile)) = - clock::record::parse_epoch(header.version, &epoch_content) + clock::record::parse_epoch(header.version, &epoch_content, clk_ts) { if let Some(e) = clk_rec.get_mut(&epoch) { e.insert(key, profile); diff --git a/rinex/src/tests/clock.rs b/rinex/src/tests/clock.rs index 60b603797..e249806e1 100644 --- a/rinex/src/tests/clock.rs +++ b/rinex/src/tests/clock.rs @@ -12,7 +12,7 @@ mod test { assert_eq!(rinex.epoch().count(), 10); for (epoch, content) in rinex.precise_clock() { - let (y, m, d, hh, mm, ss, _) = epoch.to_gregorian_utc(); + let epoch_str = epoch.to_string(); for (key, profile) in content { if let Some(sv) = key.clock_type.as_sv() { match sv { @@ -21,8 +21,8 @@ mod test { prn: 10, } => { assert_eq!(key.profile_type, ClockProfileType::AS); - match (y, m, d, hh, mm, ss) { - (2019, 1, 8, 0, 1, 30) => { + match epoch_str.as_str() { + "2019-01-08T00:01:30 GPST" => { assert_eq!(profile.bias, 0.391709678221E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -30,7 +30,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 2, 0) => { + "2019-01-08T00:02:00 GPST" => { assert_eq!(profile.bias, 0.391708653726E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -46,8 +46,8 @@ mod test { prn: 21, } => { assert_eq!(key.profile_type, ClockProfileType::AS); - match (y, m, d, hh, mm, ss) { - (2019, 1, 8, 0, 0, 0) => { + match epoch_str.as_str() { + "2019-01-08T00:00:00 GPST" => { assert_eq!(profile.bias, -0.243172599885E-04); assert_eq!(profile.bias_dev, Some(0.850129218038E-11)); assert!(profile.drift.is_none()); @@ -55,7 +55,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 0, 30) => { + "2019-01-08T00:00:30 GPST" => { assert_eq!(profile.bias, -0.243173099640E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -63,7 +63,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 1, 0) => { + "2019-01-08T00:01:00 GPST" => { assert_eq!(profile.bias, -0.243174034292E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -71,7 +71,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 1, 30) => { + "2019-01-08T00:01:30 GPST" => { assert_eq!(profile.bias, -0.243174284491E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -79,7 +79,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 2, 0) => { + "2019-01-08T00:02:00 GPST" => { assert_eq!(profile.bias, -0.243175702770E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -87,7 +87,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 2, 30) => { + "2019-01-08T00:02:30 GPST" => { assert_eq!(profile.bias, -0.243176490245E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -95,7 +95,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 3, 0) => { + "2019-01-08T00:03:00 GPST" => { assert_eq!(profile.bias, -0.243176769102E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -103,7 +103,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 0, 3, 30) => { + "2019-01-08T00:03:30 GPST" => { assert_eq!(profile.bias, -0.243177259494E-04); assert!(profile.bias_dev.is_none()); assert!(profile.drift.is_none()); @@ -111,7 +111,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - (2019, 1, 8, 10, 0, 0) => { + "2019-01-08T10:00:00 GPST" => { assert_eq!(profile.bias, -0.243934947986E-04); assert_eq!(profile.bias_dev, Some(0.846286338370E-11)); assert!(profile.drift.is_none()); diff --git a/rinex/src/tests/mod.rs b/rinex/src/tests/mod.rs index 8e4a5b41f..ff68a8cb0 100644 --- a/rinex/src/tests/mod.rs +++ b/rinex/src/tests/mod.rs @@ -18,9 +18,13 @@ mod masking; mod merge; #[cfg(feature = "meteo")] mod meteo; +#[cfg(feature = "nav")] mod nav; +#[cfg(feature = "obs")] mod obs; mod parsing; mod production; +#[cfg(feature = "processing")] mod sampling; +#[cfg(feature = "processing")] mod smoothing; diff --git a/rinex/src/tests/parsing.rs b/rinex/src/tests/parsing.rs index 14090379e..c2b75ac0e 100644 --- a/rinex/src/tests/parsing.rs +++ b/rinex/src/tests/parsing.rs @@ -181,13 +181,6 @@ mod test { assert!(rinex.header.clock.is_some(), "badly formed CLK RINEX"); assert!(rinex.epoch().count() > 0); // all files have content let record = rinex.record.as_clock().unwrap(); - for (e, _) in record { - assert!( - e.time_scale == TimeScale::UTC, - "wrong {} timescale for a CLOCK RINEX", - e.time_scale - ); - } }, "IONEX" => { assert!(rinex.is_ionex()); From 880b0e83ad2d2e53128e1fec9e446e91ec7d348b Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 12 May 2024 18:21:35 +0200 Subject: [PATCH 2/9] clk rinex Signed-off-by: Guillaume W. Bres --- rinex-cli/Cargo.toml | 8 +++----- rinex-cli/src/positioning/interp/time.rs | 14 ++++++-------- rinex-cli/src/positioning/mod.rs | 4 ---- rinex/src/record.rs | 19 ++++++++++++++----- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/rinex-cli/Cargo.toml b/rinex-cli/Cargo.toml index 87ca776bc..291fff0b4 100644 --- a/rinex-cli/Cargo.toml +++ b/rinex-cli/Cargo.toml @@ -33,17 +33,15 @@ clap = { version = "4.4.13", features = ["derive", "color"] } hifitime = { version = "3.9.0", features = ["serde", "std"] } gnss-rs = { version = "2.1.3" , features = ["serde"] } rinex = { path = "../rinex", version = "=0.16.1", features = ["full"] } +plotly = { git = "https://github.com/plotly/plotly.rs", branch = "main" } rinex-qc = { path = "../rinex-qc", version = "=0.1.14", features = ["serde"] } sp3 = { path = "../sp3", version = "=1.0.8", features = ["serde", "flate2"] } serde = { version = "1.0", default-features = false, features = ["derive"] } -# plotly -plotly = "0.8.4" -# plotly = { git = "https://github.com/gwbres/plotly", branch = "density-mapbox" } # solver -gnss-rtk = { version = "0.4.4", features = ["serde"] } -# gnss-rtk = { path = "../../rtk-rs/gnss-rtk", features = ["serde"] } +# gnss-rtk = { version = "0.4.4", features = ["serde"] } +gnss-rtk = { path = "../../rtk-rs/gnss-rtk", features = ["serde"] } # gnss-rtk = { git = "https://github.com/rtk-rs/gnss-rtk", branch = "main", features = ["serde"] } # cggtts diff --git a/rinex-cli/src/positioning/interp/time.rs b/rinex-cli/src/positioning/interp/time.rs index fd654c847..ab844f492 100644 --- a/rinex-cli/src/positioning/interp/time.rs +++ b/rinex-cli/src/positioning/interp/time.rs @@ -3,6 +3,7 @@ use crate::cli::Context; use gnss_rtk::prelude::{Duration, Epoch, SV}; use std::collections::HashMap; +#[derive(Debug)] struct Buffer { inner: Vec<(Epoch, (f64, f64, f64))>, } @@ -81,7 +82,7 @@ impl<'a> Interpolator<'a> { .map(|(t, sv, clk)| (t, sv, (clk, 0.0_f64, 0.0_f64))), ) } else { - panic!("sp3 or clock rinex currently required"); + panic!("SP3 or CLOCK RINEX currently required"); // TODO // let brdc = ctx.data.brdc_navigation().unwrap(); // infaillible // Box::new(brdc.sv_clock()) @@ -154,7 +155,7 @@ impl<'a> Interpolator<'a> { while !self.is_feasible(t, sv) { if self.consume(1) { // end of stream - return None; + break; } } @@ -165,17 +166,15 @@ impl<'a> Interpolator<'a> { // Preserves data precision first_x = Some(t); dt = Some(Duration::from_seconds(*y)); - } - - if let Some((before_x, (before_y, _, _))) = - buf.inner.iter().filter(|(v_t, _)| *v_t <= t).last() + } else if let Some((before_x, (before_y, _, _))) = + buf.inner.iter().filter(|(v_t, _)| *v_t < t).last() { first_x = Some(*before_x); if let Some((after_x, (after_y, _, _))) = buf .inner .iter() - .filter(|(v_t, _)| *v_t > t) + .filter(|(v_t, _)| *v_t >= t) .reduce(|k, _| k) { let dx = (*after_x - *before_x).to_seconds(); @@ -188,7 +187,6 @@ impl<'a> Interpolator<'a> { if let Some(first_x) = first_x { buf.inner.retain(|(k, v)| *k >= first_x); } - dt } } diff --git a/rinex-cli/src/positioning/mod.rs b/rinex-cli/src/positioning/mod.rs index c1dc52295..7be3518e0 100644 --- a/rinex-cli/src/positioning/mod.rs +++ b/rinex-cli/src/positioning/mod.rs @@ -222,11 +222,7 @@ pub fn precise_positioning(ctx: &Context, matches: &ArgMatches) -> Result<(), Er error!("Working with different timescales in OBS/CLK RINEX is not PPP compatible and will generate tiny errors"); warn!("Consider using OBS/CLK RINEX files expressed in the same timescale for optimal results"); } - } else { - error!("Provided Clock RINEX is badly defined and will likely introduce errors in calculations"); } - } else { - error!("Provided Clock RINEX is badly defined and will likely introduce errors in calculations"); } } } diff --git a/rinex/src/record.rs b/rinex/src/record.rs index d22ffd063..3b8094a76 100644 --- a/rinex/src/record.rs +++ b/rinex/src/record.rs @@ -328,14 +328,23 @@ pub fn parse_record( }, } } - // CLOCK case - // timescale is defined in header. If not - // - This RINEX is corrupt - // - and we falsely consider GPST and introduce errors - let mut clk_ts = TimeScale::default(); + // Clock RINEX TimeScale definition. + // Modern revisions define it in header directly. + // Old revisions are once again badly defined and most likely not thought out. + // We default to GPST to "match" the case where this file is multi constellation + // and it seems that clocks steered to GPST is the most common case. + // For example NASA/CDDIS.com + // In mono constellation, we adapt to that timescale. + let mut clk_ts = TimeScale::GPST; if let Some(clk) = &header.clock { if let Some(ts) = clk.timescale { clk_ts = ts; + } else { + if let Some(constellation) = &header.constellation { + if let Some(ts) = constellation.timescale() { + clk_ts = ts; + } + } } } // IONEX case From 685f9441a6676139ab0749ef9042e731bf791185 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 12 May 2024 19:40:20 +0200 Subject: [PATCH 3/9] introducing Kf Signed-off-by: Guillaume W. Bres --- rinex-cli/config/rtk/gpst_cpp_kf.json | 18 ++++++++++++++ rinex-cli/src/positioning/cggtts/mod.rs | 23 +++++++++--------- rinex-cli/src/positioning/mod.rs | 31 ++++++++++++++++--------- 3 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 rinex-cli/config/rtk/gpst_cpp_kf.json diff --git a/rinex-cli/config/rtk/gpst_cpp_kf.json b/rinex-cli/config/rtk/gpst_cpp_kf.json new file mode 100644 index 000000000..b34e87692 --- /dev/null +++ b/rinex-cli/config/rtk/gpst_cpp_kf.json @@ -0,0 +1,18 @@ +{ + "method": "CodePPP", + "timescale": "GPST", + "interp_order": 17, + "min_sv_elev": 5.0, + "solver": { + "filter": "Kalman", + "gdop_threshold": 10.0 + }, + "modeling": { + "iono_delay": true, + "tropo_delay": true, + "earth_rotation": true, + "sv_clock_bias": true, + "sv_total_group_delay": true, + "relativistic_clock_bias": true + } +} diff --git a/rinex-cli/src/positioning/cggtts/mod.rs b/rinex-cli/src/positioning/cggtts/mod.rs index 7f239e28d..84adf6e66 100644 --- a/rinex-cli/src/positioning/cggtts/mod.rs +++ b/rinex-cli/src/positioning/cggtts/mod.rs @@ -223,18 +223,17 @@ where match solver.cfg.method { Method::SPP => {}, // nothing to do Method::CodePPP => { - let (freq_to_match, code_to_match) = match carrier { - Carrier::L1 => ( - Carrier::L2.frequency(), - Observable::from_str("C2C").unwrap(), - ), - _ => ( - Carrier::L1.frequency(), - Observable::from_str("C1C").unwrap(), - ), - }; - for (observable, data) in observations { - if *observable == code_to_match { + // Attach secondary PR + for (second_obs, second_data) in observations { + let rhs_carrier = + Carrier::from_observable(sv.constellation, second_obs); + if rhs_carrier.is_err() { + continue; + } + let rhs_carrier = rhs_carrier.unwrap(); + let rtk_carrier = cast_rtk_carrier(rhs_carrier); + + if second_obs.is_pseudorange_observable() && rhs_carrier != carrier { codes.push(Observation { value: data.obs, carrier: rtk_carrier, diff --git a/rinex-cli/src/positioning/mod.rs b/rinex-cli/src/positioning/mod.rs index 7be3518e0..7f6c0bc25 100644 --- a/rinex-cli/src/positioning/mod.rs +++ b/rinex-cli/src/positioning/mod.rs @@ -47,7 +47,7 @@ pub fn cast_rtk_carrier(carrier: Carrier) -> RTKCarrier { Carrier::L5 => RTKCarrier::L5, Carrier::L6 => RTKCarrier::L6, Carrier::E1 => RTKCarrier::E1, - Carrier::E5 => RTKCarrier::E5, + Carrier::E5 | Carrier::E5a | Carrier::E5b => RTKCarrier::E5, Carrier::E6 => RTKCarrier::E6, Carrier::L1 | _ => RTKCarrier::L1, } @@ -171,18 +171,34 @@ pub fn ng_model(nav: &Rinex, t: Epoch) -> Option { pub fn precise_positioning(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { /* Load customized config script, or use defaults */ - let mut cfg = match matches.get_one::("cfg") { + let cfg = match matches.get_one::("cfg") { Some(fp) => { let content = read_to_string(fp) .unwrap_or_else(|e| panic!("failed to read configuration: {}", e)); - let cfg = serde_json::from_str(&content) + let mut cfg: Config = serde_json::from_str(&content) .unwrap_or_else(|e| panic!("failed to parse configuration: {}", e)); + + /* + * CGGTTS special case + */ + if matches.get_flag("cggtts") { + cfg.sol_type = PVTSolutionType::TimeOnly; + } + info!("Using custom solver configuration: {:#?}", cfg); cfg }, None => { let method = Method::default(); - let cfg = Config::static_preset(method); + let mut cfg = Config::static_preset(method); + + /* + * CGGTTS special case + */ + if matches.get_flag("cggtts") { + cfg.sol_type = PVTSolutionType::TimeOnly; + } + info!("Using {:?} default preset: {:#?}", method, cfg); cfg }, @@ -229,13 +245,6 @@ pub fn precise_positioning(ctx: &Context, matches: &ArgMatches) -> Result<(), Er } } - /* - * CGGTTS special case - */ - if matches.get_flag("cggtts") { - cfg.sol_type = PVTSolutionType::TimeOnly; - } - let orbit = RefCell::new(OrbitInterpolator::from_ctx( ctx, cfg.interp_order, From e52a152548d0d51963b07560564cfd598e362b25 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 12 May 2024 20:14:28 +0200 Subject: [PATCH 4/9] fix timescale in most clock rinex cases Signed-off-by: Guillaume W. Bres --- rinex/src/tests/clock.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/rinex/src/tests/clock.rs b/rinex/src/tests/clock.rs index e249806e1..c867b7331 100644 --- a/rinex/src/tests/clock.rs +++ b/rinex/src/tests/clock.rs @@ -119,7 +119,7 @@ mod test { assert!(profile.drift_change.is_none()); assert!(profile.drift_change_dev.is_none()); }, - _ => panic!("parsed bad epoch"), + _ => {}, } }, _ => {}, @@ -197,7 +197,7 @@ mod test { assert_eq!(rinex.epoch().count(), 1); for (epoch, content) in rinex.precise_clock() { - assert_eq!(*epoch, Epoch::from_str("1994-07-14T20:59:00 UTC").unwrap()); + assert_eq!(*epoch, Epoch::from_str("1994-07-14T20:59:00 GPST").unwrap()); for (key, profile) in content { match key.profile_type { ClockProfileType::AR => { @@ -306,15 +306,15 @@ mod test { "R08" => { for (epoch_str, expected) in [ ( - "2019-01-08T00:00:01 UTC", + "2019-01-08T00:00:01 GPST", 29.0 / 30.0 * 0.196700157094E-04 + 1.0 / 30.0 * 0.196699240287E-04, ), ( - "2019-01-08T00:00:15 UTC", + "2019-01-08T00:00:15 GPST", 15.0 / 30.0 * 0.196700157094E-04 + 15.0 / 30.0 * 0.196699240287E-04, ), ( - "2019-01-08T00:00:29 UTC", + "2019-01-08T00:00:29 GPST", 1.0 / 30.0 * 0.196700157094E-04 + 29.0 / 30.0 * 0.196699240287E-04, ), ] { @@ -331,15 +331,15 @@ mod test { "G30" => { for (epoch_str, expected) in [ ( - "2019-01-08T00:00:01 UTC", + "2019-01-08T00:00:01 GPST", 29.0 / 30.0 * -0.323009083512E-04 + 1.0 / 30.0 * -0.323010911710E-04, ), ( - "2019-01-08T00:00:15 UTC", + "2019-01-08T00:00:15 GPST", 15.0 / 30.0 * -0.323009083512E-04 + 15.0 / 30.0 * -0.323010911710E-04, ), ( - "2019-01-08T00:00:29 UTC", + "2019-01-08T00:00:29 GPST", 1.0 / 30.0 * -0.323009083512E-04 + 29.0 / 30.0 * -0.323010911710E-04, ), ] { @@ -356,15 +356,15 @@ mod test { "R10" => { for (epoch_str, expected) in [ ( - "2019-01-08T00:01:33 UTC", + "2019-01-08T00:01:33 GPST", 27.0 / 30.0 * 0.391709678221E-04 + 3.0 / 30.0 * 0.391708653726E-04, ), ( - "2019-01-08T00:01:44 UTC", + "2019-01-08T00:01:44 GPST", 16.0 / 30.0 * 0.391709678221E-04 + 14.0 / 30.0 * 0.391708653726E-04, ), ( - "2019-01-08T00:01:57 UTC", + "2019-01-08T00:01:57 GPST", 3.0 / 30.0 * 0.391709678221E-04 + 27.0 / 30.0 * 0.391708653726E-04, ), ] { From 626585db774babbad1f1a02381dc133740089a5f Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 12 May 2024 20:32:17 +0200 Subject: [PATCH 5/9] bump version Signed-off-by: Guillaume W. Bres --- rinex-cli/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rinex-cli/Cargo.toml b/rinex-cli/Cargo.toml index 291fff0b4..cf8669ba0 100644 --- a/rinex-cli/Cargo.toml +++ b/rinex-cli/Cargo.toml @@ -40,8 +40,8 @@ serde = { version = "1.0", default-features = false, features = ["derive"] } # solver -# gnss-rtk = { version = "0.4.4", features = ["serde"] } -gnss-rtk = { path = "../../rtk-rs/gnss-rtk", features = ["serde"] } +gnss-rtk = { version = "0.4.5", features = ["serde"] } +# gnss-rtk = { path = "../../rtk-rs/gnss-rtk", features = ["serde"] } # gnss-rtk = { git = "https://github.com/rtk-rs/gnss-rtk", branch = "main", features = ["serde"] } # cggtts From 2d1d73fa87727f7d3aeeba67eb71a23aebc45486 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 13 May 2024 07:57:43 +0200 Subject: [PATCH 6/9] fix warnings Signed-off-by: Guillaume W. Bres --- rinex-cli/src/cli/graph.rs | 11 +++++++---- rinex-cli/src/graph/record/ionex.rs | 4 ++-- rinex-cli/src/positioning/cggtts/mod.rs | 4 +--- rinex-cli/src/positioning/interp/orbit.rs | 3 +-- rinex-cli/src/positioning/interp/time.rs | 4 ++-- rinex-cli/src/positioning/ppp/mod.rs | 11 +++++------ 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/rinex-cli/src/cli/graph.rs b/rinex-cli/src/cli/graph.rs index 31a916025..966eb789d 100644 --- a/rinex-cli/src/cli/graph.rs +++ b/rinex-cli/src/cli/graph.rs @@ -35,11 +35,11 @@ using our toolbox as data parser and preprocessor and inject the results to thir OBS RINEX gives GNSS signals observations, but we also support Meteo RINEX and DORIS (special observation) RINEX. Example (1): render GNSS signals (all of them, whether it be Phase or PR) for GPS. -Use CSV for extract and export as well: +Use CSV for extract as well: ./target/release/rinex-cli \\ -f test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \\ - -g --obs --csv + -P GPS -g --obs --csv Example (2): render meteo sensor observations similary. @@ -54,7 +54,7 @@ Example (3): render DORIS observations similarly. -g --obs --csv Example (4): render OBS + Meteo combination at once. -RINEX-Cli allows loading OBS + Meteo in one session. +RINEX-Cli allows loading OBS and Meteo in one session. In graph mode, this means we can render both in a single run. ./target/release/rinex-cli \\ @@ -175,7 +175,10 @@ It is the temporal equuivalent to |BRDC-SP3| requested with --sp3-residual.") Arg::new("tec") .long("tec") .action(ArgAction::SetTrue) - .help("Plot the TEC map. Requires at least one IONEX file."), + .help("Plot the TEC map. Requires at least one IONEX file. See --help") + .long_help("Plot the worldwide TEC map, usually presented in 24hr time frame. +Example: +rinex-cli -f test_resources/IONEX/V1/CKMG0080.09I.gz -g --tec") ) .arg( Arg::new("ionod") diff --git a/rinex-cli/src/graph/record/ionex.rs b/rinex-cli/src/graph/record/ionex.rs index a2d4fab27..0ecae3a57 100644 --- a/rinex-cli/src/graph/record/ionex.rs +++ b/rinex-cli/src/graph/record/ionex.rs @@ -9,11 +9,11 @@ use rinex::prelude::Rinex; pub fn plot_tec_map(data: &Rinex, _borders: ((f64, f64), (f64, f64)), plot_ctx: &mut PlotContext) { let _cmap = colorous::TURBO; - let hover_text: Vec = data.epoch().map(|e| e.to_string()).collect(); + // let hover_text: Vec = data.epoch().map(|e| e.to_string()).collect(); /* * TEC map visualization * plotly-rs has no means to animate plots at the moment - * therefore.. we create one plot for all remaining Epochs + * therefore.. we create one plot for each individual Epoch */ for epoch in data.epoch() { let lat: Vec<_> = data diff --git a/rinex-cli/src/positioning/cggtts/mod.rs b/rinex-cli/src/positioning/cggtts/mod.rs index 84adf6e66..fb95bcc37 100644 --- a/rinex-cli/src/positioning/cggtts/mod.rs +++ b/rinex-cli/src/positioning/cggtts/mod.rs @@ -21,7 +21,6 @@ use rtk::prelude::{ IonosphereBias, Method, Observation, - PVTSolutionType, Solver, TroposphereBias, //TimeScale }; @@ -163,7 +162,6 @@ where } let carrier = carrier.unwrap(); - let frequency = carrier.frequency(); let rtk_carrier = cast_rtk_carrier(carrier); let mut code = Option::::None; @@ -235,7 +233,7 @@ where if second_obs.is_pseudorange_observable() && rhs_carrier != carrier { codes.push(Observation { - value: data.obs, + value: second_data.obs, carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, }); diff --git a/rinex-cli/src/positioning/interp/orbit.rs b/rinex-cli/src/positioning/interp/orbit.rs index 5629663cf..1602c5c9e 100644 --- a/rinex-cli/src/positioning/interp/orbit.rs +++ b/rinex-cli/src/positioning/interp/orbit.rs @@ -1,6 +1,5 @@ use super::Buffer as BufferTrait; use crate::cli::Context; -use hifitime::Duration; use std::collections::HashMap; use gnss_rtk::prelude::{ @@ -233,7 +232,7 @@ impl<'a> Interpolator<'a> { let mut polynomials = (0.0_f64, 0.0_f64, 0.0_f64); let mut out = Option::::None; - for (index, (buf_t, buf_v)) in buf.inner.iter().enumerate() { + for (index, (buf_t, _)) in buf.inner.iter().enumerate() { if *buf_t > t { break; } diff --git a/rinex-cli/src/positioning/interp/time.rs b/rinex-cli/src/positioning/interp/time.rs index ab844f492..1b08887af 100644 --- a/rinex-cli/src/positioning/interp/time.rs +++ b/rinex-cli/src/positioning/interp/time.rs @@ -159,7 +159,7 @@ impl<'a> Interpolator<'a> { } } - let mut buf = self.buffers.get_mut(&sv)?; + let buf = self.buffers.get_mut(&sv)?; if let Some((y, _, _)) = buf.direct_output(t) { // No need to interpolate @ t for SV @@ -185,7 +185,7 @@ impl<'a> Interpolator<'a> { } // Discard symbols that did not contribute (too old) if let Some(first_x) = first_x { - buf.inner.retain(|(k, v)| *k >= first_x); + buf.inner.retain(|(k, _)| *k >= first_x); } dt } diff --git a/rinex-cli/src/positioning/ppp/mod.rs b/rinex-cli/src/positioning/ppp/mod.rs index 9438305f9..089ece71f 100644 --- a/rinex-cli/src/positioning/ppp/mod.rs +++ b/rinex-cli/src/positioning/ppp/mod.rs @@ -14,8 +14,8 @@ mod post_process; pub use post_process::{post_process, Error as PostProcessingError}; use rtk::prelude::{ - Candidate, Epoch, InterpolationResult, IonosphereBias, Observation, PVTSolution, - PVTSolutionType, Solver, TroposphereBias, + Candidate, Epoch, InterpolationResult, IonosphereBias, Observation, PVTSolution, Solver, + TroposphereBias, }; pub fn resolve( @@ -87,27 +87,26 @@ where for (observable, data) in observations { if let Ok(carrier) = Carrier::from_observable(sv.constellation, observable) { - let frequency = carrier.frequency(); let rtk_carrier: gnss_rtk::prelude::Carrier = cast_rtk_carrier(carrier); if observable.is_pseudorange_observable() { codes.push(Observation { + value: data.obs, carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, - value: data.obs, }); } else if observable.is_phase_observable() { let lambda = carrier.wavelength(); phases.push(Observation { carrier: rtk_carrier, - snr: { data.snr.map(|snr| snr.into()) }, value: data.obs * lambda, + snr: { data.snr.map(|snr| snr.into()) }, }); } else if observable.is_doppler_observable() { dopplers.push(Observation { + value: data.obs, carrier: rtk_carrier, snr: { data.snr.map(|snr| snr.into()) }, - value: data.obs, }); } } From 157de5d144c2b93c1ea277a8fd1c852a34d24d53 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 13 May 2024 20:52:21 +0200 Subject: [PATCH 7/9] working on DORIS support Signed-off-by: Guillaume W. Bres --- rinex/src/carrier.rs | 11 +++ rinex/src/context.rs | 67 +++++++------ rinex/src/doris/mod.rs | 2 + rinex/src/lib.rs | 207 ++++++++++++++++++++++++++++++++++++---- rinex/src/observable.rs | 30 +++--- 5 files changed, 263 insertions(+), 54 deletions(-) diff --git a/rinex/src/carrier.rs b/rinex/src/carrier.rs index 13761f24e..62581c00e 100644 --- a/rinex/src/carrier.rs +++ b/rinex/src/carrier.rs @@ -731,6 +731,17 @@ impl Carrier { _ => Err(Error::UnknownSV(sv)), } } + /// Builds Self from DORIS observable + pub fn from_doris_observable(obs: &Observable) -> Result { + let obs = obs.to_string(); + if obs.contains("1") { + Ok(Self::S1) + } else if obs.contains("2") { + Ok(Self::U2) + } else { + Err(Error::UnknownObservable(obs.clone())) + } + } } #[cfg(test)] diff --git a/rinex/src/context.rs b/rinex/src/context.rs index 07eb62778..ed0f90cd6 100644 --- a/rinex/src/context.rs +++ b/rinex/src/context.rs @@ -42,7 +42,7 @@ pub enum ProductType { /// Meteo sensors data wrapped as Meteo RINEX files. MeteoObservation, /// DORIS measurements wrapped as special RINEX observation file. - DorisRinex, + DORIS, /// Broadcast Navigation message as contained in /// Navigation RINEX files. BroadcastNavigation, @@ -51,9 +51,9 @@ pub enum ProductType { /// High precision orbital attitudes wrapped in Clock RINEX files. HighPrecisionClock, /// Antenna calibration information wrapped in ANTEX special RINEX files. - Antex, + ANTEX, /// Precise Ionosphere state wrapped in IONEX special RINEX files. - Ionex, + IONEX, } impl std::fmt::Display for ProductType { @@ -64,9 +64,9 @@ impl std::fmt::Display for ProductType { Self::BroadcastNavigation => write!(f, "Broadcast Navigation"), Self::HighPrecisionOrbit => write!(f, "High Precision Orbit (SP3)"), Self::HighPrecisionClock => write!(f, "High Precision Clock"), - Self::Antex => write!(f, "ANTEX"), - Self::Ionex => write!(f, "IONEX"), - Self::DorisRinex => write!(f, "DORIS RINEX"), + Self::ANTEX => write!(f, "ANTEX"), + Self::IONEX => write!(f, "IONEX"), + Self::DORIS => write!(f, "DORIS RINEX"), } } } @@ -78,9 +78,9 @@ impl From for ProductType { RinexType::NavigationData => Self::BroadcastNavigation, RinexType::MeteoData => Self::MeteoObservation, RinexType::ClockData => Self::HighPrecisionClock, - RinexType::IonosphereMaps => Self::Ionex, - RinexType::AntennaData => Self::Antex, - RinexType::DORIS => Self::DorisRinex, + RinexType::IonosphereMaps => Self::IONEX, + RinexType::AntennaData => Self::ANTEX, + RinexType::DORIS => Self::DORIS, } } } @@ -144,12 +144,13 @@ impl RnxContext { */ for product in [ ProductType::Observation, + ProductType::DORIS, ProductType::BroadcastNavigation, ProductType::MeteoObservation, ProductType::HighPrecisionClock, ProductType::HighPrecisionOrbit, - ProductType::Ionex, - ProductType::Antex, + ProductType::IONEX, + ProductType::ANTEX, ] { if let Some(paths) = self.files(product) { /* @@ -233,18 +234,22 @@ impl RnxContext { pub fn rinex(&self, product: ProductType) -> Option<&Rinex> { self.data(product)?.as_rinex() } - /// Returns reference to inner SP3 data - pub fn sp3(&self) -> Option<&SP3> { - self.data(ProductType::HighPrecisionOrbit)?.as_sp3() - } /// Returns mutable reference to inner RINEX data of given category pub fn rinex_mut(&mut self, product: ProductType) -> Option<&mut Rinex> { self.data_mut(product)?.as_mut_rinex() } + /// Returns reference to inner SP3 data + pub fn sp3(&self) -> Option<&SP3> { + self.data(ProductType::HighPrecisionOrbit)?.as_sp3() + } /// Returns reference to inner [ProductType::Observation] data pub fn observation(&self) -> Option<&Rinex> { self.data(ProductType::Observation)?.as_rinex() } + /// Returns reference to inner [ProductType::DORIS] RINEX data + pub fn doris(&self) -> Option<&Rinex> { + self.data(ProductType::DORIS)?.as_rinex() + } /// Returns reference to inner [ProductType::BroadcastNavigation] data pub fn brdc_navigation(&self) -> Option<&Rinex> { self.data(ProductType::BroadcastNavigation)?.as_rinex() @@ -257,18 +262,22 @@ impl RnxContext { pub fn clock(&self) -> Option<&Rinex> { self.data(ProductType::HighPrecisionClock)?.as_rinex() } - /// Returns reference to inner [ProductType::Antex] data + /// Returns reference to inner [ProductType::ANTEX] data pub fn antex(&self) -> Option<&Rinex> { - self.data(ProductType::Antex)?.as_rinex() + self.data(ProductType::ANTEX)?.as_rinex() } - /// Returns reference to inner [ProductType::Ionex] data + /// Returns reference to inner [ProductType::IONEX] data pub fn ionex(&self) -> Option<&Rinex> { - self.data(ProductType::Ionex)?.as_rinex() + self.data(ProductType::IONEX)?.as_rinex() } /// Returns mutable reference to inner [ProductType::Observation] data pub fn observation_mut(&mut self) -> Option<&mut Rinex> { self.data_mut(ProductType::Observation)?.as_mut_rinex() } + /// Returns mutable reference to inner [ProductType::DORIS] RINEX data + pub fn doris_mut(&mut self) -> Option<&mut Rinex> { + self.data_mut(ProductType::DORIS)?.as_mut_rinex() + } /// Returns mutable reference to inner [ProductType::Observation] data pub fn brdc_navigation_mut(&mut self) -> Option<&mut Rinex> { self.data_mut(ProductType::BroadcastNavigation)? @@ -287,13 +296,13 @@ impl RnxContext { pub fn sp3_mut(&mut self) -> Option<&mut SP3> { self.data_mut(ProductType::HighPrecisionOrbit)?.as_mut_sp3() } - /// Returns mutable reference to inner [ProductType::Antex] data + /// Returns mutable reference to inner [ProductType::ANTEX] data pub fn antex_mut(&mut self) -> Option<&mut Rinex> { - self.data_mut(ProductType::Antex)?.as_mut_rinex() + self.data_mut(ProductType::ANTEX)?.as_mut_rinex() } - /// Returns mutable reference to inner [ProductType::Ionex] data + /// Returns mutable reference to inner [ProductType::IONEX] data pub fn ionex_mut(&mut self) -> Option<&mut Rinex> { - self.data_mut(ProductType::Ionex)?.as_mut_rinex() + self.data_mut(ProductType::IONEX)?.as_mut_rinex() } /// Returns true if [ProductType::Observation] are present in Self pub fn has_observation(&self) -> bool { @@ -307,6 +316,10 @@ impl RnxContext { pub fn has_sp3(&self) -> bool { self.sp3().is_some() } + /// Returns true if at least one [ProductType::DORIS] file is present + pub fn has_doris(&self) -> bool { + self.doris().is_some() + } /// Returns true if [ProductType::MeteoObservation] are present in Self pub fn has_meteo(&self) -> bool { self.meteo().is_some() @@ -430,8 +443,8 @@ impl HtmlReport for RnxContext { ProductType::MeteoObservation, ProductType::HighPrecisionOrbit, ProductType::HighPrecisionClock, - ProductType::Ionex, - ProductType::Antex, + ProductType::IONEX, + ProductType::ANTEX, ] { tr { td { @@ -469,8 +482,8 @@ impl std::fmt::Debug for RnxContext { ProductType::MeteoObservation, ProductType::HighPrecisionOrbit, ProductType::HighPrecisionClock, - ProductType::Ionex, - ProductType::Antex, + ProductType::IONEX, + ProductType::ANTEX, ] { if let Some(files) = self.files(product) { write!(f, "\n{}: ", product)?; diff --git a/rinex/src/doris/mod.rs b/rinex/src/doris/mod.rs index 962d11da2..506df9669 100644 --- a/rinex/src/doris/mod.rs +++ b/rinex/src/doris/mod.rs @@ -33,6 +33,8 @@ pub enum Error { #[derive(Debug, Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HeaderFields { + /// Name of the DORIS satellite + pub satellite: String, /// Time of First Measurement, expressed in TAI timescale. pub time_of_first_obs: Option, /// Time of Last Measurement, expressed in TAI timescale. diff --git a/rinex/src/lib.rs b/rinex/src/lib.rs index 4d8aab728..d6f1fd0ea 100644 --- a/rinex/src/lib.rs +++ b/rinex/src/lib.rs @@ -59,13 +59,16 @@ use std::io::Write; //, Read}; use std::path::Path; use std::str::FromStr; +use itertools::Itertools; use thiserror::Error; use antex::{Antenna, AntennaSpecific, FrequencyDependentData}; +use doris::record::ObservationData as DorisObservationData; use epoch::epoch_decompose; use ionex::TECPlane; +use navigation::NavFrame; use observable::Observable; -use observation::Crinex; +use observation::{Crinex, ObservationData}; use version::Version; use production::{DataSource, DetailedProductionAttributes, ProductionAttributes, FFU, PPU}; @@ -81,6 +84,7 @@ pub mod prelude { pub use crate::clock::{ClockKey, ClockProfile, ClockProfileType, ClockType, WorkClock}; #[cfg(feature = "sp3")] pub use crate::context::{ProductType, RnxContext}; + pub use crate::cospar::COSPAR; pub use crate::domes::Domes; #[cfg(feature = "doris")] pub use crate::doris::Station; @@ -681,25 +685,24 @@ impl Rinex { /// ``` /// use rinex::prelude::*; /// - /// // Parse one file that does not follow naming conventions + /// // Parse file that does not follow naming conventions /// let rinex = Rinex::from_file("../test_resources/MET/V4/example1.txt"); /// assert!(rinex.is_ok()); // As previously stated, we totally accept that /// let rinex = rinex.unwrap(); /// - /// // The standard file name generator has no means to generate something correct. + /// // The standard filename generator has no means to generate something correct. /// let standard_name = rinex.standard_filename(true, None, None); /// assert_eq!(standard_name, "XXXX0070.21M"); /// - /// // We use the smart attributes detector as custom attributes + /// // Now use the smart attributes detector as custom attributes /// let guessed = rinex.guess_production_attributes(); /// let standard_name = rinex.standard_filename(true, None, Some(guessed.clone())); /// - /// // we get a perfect shortened name + /// // Short name are always correctly determined /// assert_eq!(standard_name, "bako0070.21M"); /// - /// // If we ask for a (modern) long standard filename, we mostly get it right, - /// // but some fields like the Country code can only be determined from the original filename, - /// // so we have no means to receover them. + /// // Modern (lengthy) names have fields like the Country code that cannot be recovered + /// // if the original file did not follow standard conventions itself. /// let standard_name = rinex.standard_filename(false, None, Some(guessed.clone())); /// assert_eq!(standard_name, "bako00XXX_U_20210070000_00U_MM.rnx"); /// ``` @@ -1108,7 +1111,9 @@ impl Rinex { /// assert!(rnx.to_file("test.rnx").is_ok()); /// ``` /// Other useful links are: - /// * our Production settings customization infrastructure [Self:: + /// * [Self::standard_filename] to generate a standardized filename + /// * [Self::guess_production_attributes] helps generate standardized filenames for + /// files that do not follow naming conventions pub fn to_file(&self, path: &str) -> Result<(), Error> { let mut writer = BufferedWriter::new(path)?; write!(writer, "{}", self.header)?; @@ -1291,12 +1296,7 @@ impl Rinex { /* * Methods that return an Iterator exclusively. * These methods are used to browse data easily and efficiently. - * It includes Format dependent extraction methods : one per format. */ -use crate::navigation::NavFrame; -use itertools::Itertools; // .unique() -use observation::ObservationData; - impl Rinex { pub fn epoch(&self) -> Box + '_> { if let Some(r) = self.record.as_obs() { @@ -1679,6 +1679,24 @@ impl Rinex { .flat_map(|record| record.iter()), ) } + /// DORIS special RINEX iterator + pub fn doris( + &self, + ) -> Box< + dyn Iterator< + Item = ( + &(Epoch, EpochFlag), + &BTreeMap>, + ), + > + '_, + > { + Box::new( + self.record + .as_doris() + .into_iter() + .flat_map(|record| record.iter()), + ) + } /// ANTEX antennas specifications browsing pub fn antennas( &self, @@ -1850,8 +1868,7 @@ impl Rinex { observations.iter().filter_map(|(observable, obsdata)| { if observable.is_phase_observable() { if let Some(header) = &self.header.obs { - // apply a scaling, if any, otherwise : leave data untouched - // to preserve its precision + // apply a scaling (if any), otherwise preserve data precision if let Some(scaling) = header.scaling(sv.constellation, observable.clone()) { @@ -3467,6 +3484,164 @@ impl Rinex { } } +/* + * DORIS special features + */ +#[cfg(feature = "doris")] +#[cfg_attr(docrs, doc(cfg(feature = "doris")))] +impl Rinex { + /// Returns Stations Iterator + pub fn stations(&self) -> Box + '_> { + if let Some(doris) = &self.header.doris { + Box::new(doris.stations.iter()) + } else { + Box::new([].iter()) + } + } + /// Returns temperature data iterator, per DORIS station. Values expressed in Celcius degrees. + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for (epoch, station, value) in rinex.doris_temperature() { + /// println!("{}@{}: {} °C", station.domes, epoch, value); + /// } + pub fn doris_temperature(&self) -> Box + '_> { + Box::new(self.doris().flat_map(|((epoch, _), stations)| { + stations.iter().flat_map(move |(station, observables)| { + observables.iter().filter_map(move |(observable, data)| { + if *observable == Observable::Temperature { + Some((*epoch, station, data.value)) + } else { + None + } + }) + }) + })) + } + /// Returns pressure data iterator, per DORIS station. Values expressed in hPa. + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for (epoch, station, value) in rinex.doris_pressure() { + /// println!("{}@{}: {} hPa", station.domes, epoch, value); + /// } + pub fn doris_pressure(&self) -> Box + '_> { + Box::new(self.doris().flat_map(|((epoch, _), stations)| { + stations.iter().flat_map(move |(station, observables)| { + observables.iter().filter_map(move |(observable, data)| { + if *observable == Observable::Pressure { + Some((*epoch, station, data.value)) + } else { + None + } + }) + }) + })) + } + /// Humidity saturation rate Iterator, per DORIS station. Values expressed in percent. + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for (epoch, station, value) in rinex.doris_moisture() { + /// println!("{}@{}: {}%", station.domes, epoch, value); + /// } + pub fn doris_moisture(&self) -> Box + '_> { + Box::new(self.doris().flat_map(|((epoch, _), stations)| { + stations.iter().flat_map(move |(station, observables)| { + observables.iter().filter_map(move |(observable, data)| { + if *observable == Observable::HumidityRate { + Some((*epoch, station, data.value)) + } else { + None + } + }) + }) + })) + } + /// Returns phase data iterator, per DORIS station. Values expressed in meters. + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for (epoch, station, code, value) in rinex.doris_phase() { + /// println!("{} {}@{}: {}", station.domes, code, epoch, value); + /// } + pub fn doris_phase( + &self, + ) -> Box + '_> { + Box::new(self.doris().flat_map(|((epoch, _), stations)| { + stations.iter().flat_map(move |(station, observables)| { + observables.iter().filter_map(move |(observable, data)| { + if observable.is_phase_observable() { + Some((*epoch, station, observable, data.value)) + } else { + None + } + }) + }) + })) + } + /// (High precision) Pseudo Range Iterator, per DORIS station. Values expressed in meters. + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for (epoch, station, code, value) in rinex.doris_pseudo_range() { + /// println!("{} {}@{}: {}m", station.domes, code, epoch, value); + /// } + pub fn doris_pseudo_range( + &self, + ) -> Box + '_> { + Box::new(self.doris().flat_map(move |((epoch, _), stations)| { + stations.iter().flat_map(move |(station, observables)| { + observables.iter().filter_map(move |(observable, data)| { + if observable.is_pseudorange_observable() { + if let Some(header) = &self.header.doris { + // apply a scaling (if any), otherwise preserve data precision + if let Some(scaling) = header.scaling.get(&observable) { + Some((*epoch, station, observable, data.value / *scaling as f64)) + } else { + Some((*epoch, station, observable, data.value)) + } + } else { + Some((*epoch, station, observable, data.value)) + } + } else { + None + } + }) + }) + })) + } + /// Returns received signal power Iterator, as observed at each DORIS stations. + /// Values expressed in [dBm]. + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for (epoch, station, code, value) in rinex.doris_power() { + /// println!("{} {}@{}: {} dBm", station.domes, code, epoch, value); + /// } + pub fn doris_power( + &self, + ) -> Box + '_> { + Box::new(self.doris().flat_map(|((epoch, _), stations)| { + stations.iter().flat_map(move |(station, observables)| { + observables.iter().filter_map(move |(observable, data)| { + if observable.is_power_observable() { + Some((*epoch, station, observable, data.value)) + } else { + None + } + }) + }) + })) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/rinex/src/observable.rs b/rinex/src/observable.rs index af8b777ce..6a98e3ed5 100644 --- a/rinex/src/observable.rs +++ b/rinex/src/observable.rs @@ -19,8 +19,10 @@ pub enum Observable { Phase(String), /// Doppler shift observation Doppler(String), - /// SSI observation + /// SSI: Receiver signal strength observation [dB] SSI(String), + /// Received Power [dBm] + Power(String), /// Pseudo range observation PseudoRange(String), /// Channel number Pseudo Observable. @@ -48,8 +50,8 @@ pub enum Observable { RainIncrement, /// Hail Indicator HailIndicator, - /// Frequency Offset (dimensionless) - FrequencyOffset, + /// Frequency Ratio (dimensionless) + FrequencyRatio, } impl Default for Observable { @@ -71,6 +73,9 @@ impl Observable { pub fn is_ssi_observable(&self) -> bool { matches!(self, Self::SSI(_)) } + pub fn is_power_observable(&self) -> bool { + matches!(self, Self::Power(_)) + } pub fn is_channel_number(&self) -> bool { matches!(self, Self::ChannelNumber(_)) } @@ -288,12 +293,13 @@ impl std::fmt::Display for Observable { Self::WindSpeed => write!(f, "WS"), Self::RainIncrement => write!(f, "RI"), Self::HailIndicator => write!(f, "HI"), - Self::FrequencyOffset => write!(f, "F"), - Self::PseudoRange(c) => write!(f, "{}", c), - Self::Phase(c) => write!(f, "{}", c), - Self::Doppler(c) => write!(f, "{}", c), - Self::SSI(c) => write!(f, "{}", c), - Self::ChannelNumber(x) => write!(f, "{}", x), + Self::FrequencyRatio => write!(f, "F"), + Self::PseudoRange(c) + | Self::Phase(c) + | Self::Doppler(c) + | Self::SSI(c) + | Self::Power(c) + | Self::ChannelNumber(c) => write!(f, "{}", c), } } } @@ -307,7 +313,7 @@ impl std::str::FromStr for Observable { "P" | "PR" => Ok(Self::Pressure), "T" | "TD" => Ok(Self::Temperature), "H" | "HR" => Ok(Self::HumidityRate), - "F" => Ok(Self::FrequencyOffset), + "F" => Ok(Self::FrequencyRatio), "ZW" => Ok(Self::ZenithWetDelay), "ZD" => Ok(Self::ZenithDryDelay), "ZT" => Ok(Self::ZenithTotalDelay), @@ -322,8 +328,10 @@ impl std::str::FromStr for Observable { Ok(Self::Phase(content.to_string())) } else if content.starts_with('C') || content.starts_with('P') { Ok(Self::PseudoRange(content.to_string())) - } else if content.starts_with('S') || content.starts_with('W') { + } else if content.starts_with('S') { Ok(Self::SSI(content.to_string())) + } else if content.starts_with('W') { + Ok(Self::Power(content.to_string())) } else if content.starts_with('D') { Ok(Self::Doppler(content.to_string())) } else { From 1fd99aaaf49984d92e17bf9f05533ed471f2118e Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 13 May 2024 20:52:32 +0200 Subject: [PATCH 8/9] COSPAR has been introduced Signed-off-by: Guillaume W. Bres --- rinex/src/hardware.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rinex/src/hardware.rs b/rinex/src/hardware.rs index acfa53432..3c82fa3f6 100644 --- a/rinex/src/hardware.rs +++ b/rinex/src/hardware.rs @@ -1,5 +1,6 @@ //! Hardware: receiver, antenna informations -use gnss::prelude::SV; +use crate::prelude::{COSPAR, SV}; +use std::str::FromStr; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -202,12 +203,10 @@ impl HtmlReport for Rcvr { pub struct SvAntenna { /// vehicle this antenna is attached to pub sv: SV, - /// antenna model description + /// Antenna model description pub model: String, - /// "YYYY-XXXA" year of vehicle launch - /// XXX sequential launch vehicle - /// A: alpha numeric sequence number within launch - pub cospar: Option, + /// COSPAR launch ID code + pub cospar: Option, } impl SvAntenna { @@ -223,7 +222,9 @@ impl SvAntenna { } pub fn with_cospar(&self, c: &str) -> Self { let mut s = self.clone(); - s.cospar = Some(c.to_string()); + if let Ok(cospar) = COSPAR::from_str(c) { + s.cospar = Some(cospar); + } s } } From 3e9ec838bad028ce377e75478b0aca78249f2e6a Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 13 May 2024 21:27:36 +0200 Subject: [PATCH 9/9] introduce DORIS rinex handling in cli Signed-off-by: Guillaume W. Bres --- rinex-cli/src/cli/graph.rs | 13 +- rinex-cli/src/fops.rs | 9 +- rinex-cli/src/graph/mod.rs | 3 + rinex-cli/src/graph/record/doris.rs | 296 ++++++++++++++++++++++++++++ rinex-cli/src/graph/record/mod.rs | 2 + rinex/src/doris/mod.rs | 22 +-- rinex/src/lib.rs | 86 ++++---- 7 files changed, 370 insertions(+), 61 deletions(-) create mode 100644 rinex-cli/src/graph/record/doris.rs diff --git a/rinex-cli/src/cli/graph.rs b/rinex-cli/src/cli/graph.rs index 966eb789d..c427e7a4f 100644 --- a/rinex-cli/src/cli/graph.rs +++ b/rinex-cli/src/cli/graph.rs @@ -50,8 +50,7 @@ Example (2): render meteo sensor observations similary. Example (3): render DORIS observations similarly. ./target/release/rinex-cli \\ - -f test_resources/MET/V3/POTS00DEU_R_20232540000_01D_05M_MM.rnx.gz \\ - -g --obs --csv + -f test_resources/OR/V3/cs2rx18164.gz -g --obs --csv Example (4): render OBS + Meteo combination at once. RINEX-Cli allows loading OBS and Meteo in one session. @@ -186,14 +185,4 @@ rinex-cli -f test_resources/IONEX/V1/CKMG0080.09I.gz -g --tec") .action(ArgAction::SetTrue) .help("Plot ionospheric delay per signal & SV, at latitude and longitude of signal sampling."), ) - .next_help_heading("DORIS (requires at least one DORIS file)"). - arg( - Arg::new("acorr") - .short('a') - .long("acorr") - .action(ArgAction::SetTrue) - .help("Compute and render the autocorrelation of (precise) Pseudo Range and Dopplers from the DORIS measurement, -from all contained stations. See --help") - .long_help("TODO") - ) } diff --git a/rinex-cli/src/fops.rs b/rinex-cli/src/fops.rs index e3ad3817a..2b3706640 100644 --- a/rinex-cli/src/fops.rs +++ b/rinex-cli/src/fops.rs @@ -103,11 +103,12 @@ pub fn filegen(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { for product in [ ProductType::Observation, + ProductType::DORIS, ProductType::MeteoObservation, ProductType::BroadcastNavigation, ProductType::HighPrecisionClock, - ProductType::Ionex, - ProductType::Antex, + ProductType::IONEX, + ProductType::ANTEX, ] { if let Some(rinex) = ctx_data.rinex(product) { let prod = custom_prod_attributes(rinex, matches); @@ -180,7 +181,7 @@ pub fn split(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { ProductType::MeteoObservation, ProductType::BroadcastNavigation, ProductType::HighPrecisionClock, - ProductType::Ionex, + ProductType::IONEX, ] { if let Some(rinex) = ctx_data.rinex(product) { let (rinex_a, rinex_b) = rinex @@ -295,7 +296,7 @@ pub fn time_binning(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { ProductType::MeteoObservation, ProductType::BroadcastNavigation, ProductType::HighPrecisionClock, - ProductType::Ionex, + ProductType::IONEX, ] { // input data determination if let Some(rinex) = ctx_data.rinex(product) { diff --git a/rinex-cli/src/graph/mod.rs b/rinex-cli/src/graph/mod.rs index 824e7e14a..9af352fdd 100644 --- a/rinex-cli/src/graph/mod.rs +++ b/rinex-cli/src/graph/mod.rs @@ -545,6 +545,9 @@ pub fn graph_opmode(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { if ctx.data.has_meteo() { record::plot_meteo_observations(ctx, &mut plot_ctx, csv_export); } + if ctx.data.has_doris() { + record::plot_doris_observations(ctx, &mut plot_ctx, csv_export); + } /* save observations */ ctx.render_html("OBSERVATIONS.html", plot_ctx.to_html()); diff --git a/rinex-cli/src/graph/record/doris.rs b/rinex-cli/src/graph/record/doris.rs new file mode 100644 index 000000000..cc715e464 --- /dev/null +++ b/rinex-cli/src/graph/record/doris.rs @@ -0,0 +1,296 @@ +use crate::cli::Context; +use itertools::Itertools; +use plotly::common::{Marker, MarkerSymbol, Mode, Visible}; +use rinex::{carrier::Carrier, prelude::*}; + +use crate::graph::{build_chart_epoch_axis, PlotContext}; + +fn plot_title(observable: Observable) -> (String, String) { + if observable.is_pseudorange_observable() { + ("Pseudo Range".to_string(), "[m]".to_string()) + } else if observable.is_phase_observable() { + ("Phase".to_string(), "[m]".to_string()) + } else if observable.is_power_observable() { + ("Signal Power".to_string(), "Power [dBm]".to_string()) + } else if observable == Observable::Temperature { + ( + "Temperature (at base station)".to_string(), + "T [°C]".to_string(), + ) + } else if observable == Observable::Pressure { + ( + "Pressure (at base station)".to_string(), + "P [hPa]".to_string(), + ) + } else if observable == Observable::HumidityRate { + ( + "Humidity (at base station)".to_string(), + "Saturation Rate [%]".to_string(), + ) + } else if observable == Observable::FrequencyRatio { + ( + "RX Clock Offset (f(t)-f0)/f0".to_string(), + "n.a".to_string(), + ) + } else { + unreachable!("unexpected DORIS observable {}", observable); + } +} + +/* + * Plots given DORIS RINEX content + */ +pub fn plot_doris_observations(ctx: &Context, plot_ctx: &mut PlotContext, _csv_export: bool) { + let doris = ctx.data.doris().unwrap(); // infaillible + + // Per observable + for observable in doris.observable().sorted() { + // Trick to present S1/U2 on the same plot + let marker_symbol = if observable.is_phase_observable() + || observable.is_pseudorange_observable() + || observable.is_power_observable() + { + let obs_str = observable.to_string(); + if obs_str.contains("1") { + let (plot_title, y_title) = plot_title(observable.clone()); + plot_ctx.add_timedomain_plot(&plot_title, &y_title); + MarkerSymbol::Circle + } else { + MarkerSymbol::Diamond + } + } else { + let (plot_title, y_title) = plot_title(observable.clone()); + plot_ctx.add_timedomain_plot(&plot_title, &y_title); + MarkerSymbol::Circle + }; + + // Per station + for (station_index, station) in doris.stations().sorted().enumerate() { + if *observable == Observable::Temperature { + let x = doris + .doris_temperature() + .filter_map(|(t_i, station_i, _data)| { + if station_i == station { + Some(t_i) + } else { + None + } + }) + .collect::>(); + let y = doris + .doris_temperature() + .filter_map(|(_t_i, station_i, value)| { + if station_i == station { + Some(value) + } else { + None + } + }) + .collect::>(); + let trace = build_chart_epoch_axis(&station.label, Mode::Markers, x, y) + .marker(Marker::new().symbol(marker_symbol.clone())) + .visible({ + if station_index < 3 { + Visible::True + } else { + Visible::LegendOnly + } + }); + plot_ctx.add_trace(trace); + } else if *observable == Observable::Pressure { + let x = doris + .doris_pressure() + .filter_map(|(t_i, station_i, _data)| { + if station_i == station { + Some(t_i) + } else { + None + } + }) + .collect::>(); + let y = doris + .doris_pressure() + .filter_map(|(_t_i, station_i, value)| { + if station_i == station { + Some(value) + } else { + None + } + }) + .collect::>(); + let trace = build_chart_epoch_axis(&station.label, Mode::Markers, x, y) + .marker(Marker::new().symbol(marker_symbol.clone())) + .visible({ + if station_index < 3 { + Visible::True + } else { + Visible::LegendOnly + } + }); + plot_ctx.add_trace(trace); + } else if *observable == Observable::HumidityRate { + let x = doris + .doris_humidity() + .filter_map(|(t_i, station_i, _data)| { + if station_i == station { + Some(t_i) + } else { + None + } + }) + .collect::>(); + let y = doris + .doris_humidity() + .filter_map(|(_t_i, station_i, value)| { + if station_i == station { + Some(value) + } else { + None + } + }) + .collect::>(); + let trace = build_chart_epoch_axis(&station.label, Mode::Markers, x, y) + .marker(Marker::new().symbol(marker_symbol.clone())) + .visible({ + if station_index < 3 { + Visible::True + } else { + Visible::LegendOnly + } + }); + plot_ctx.add_trace(trace); + // TODO + // } else if *observable == Observable::FrequencyRatio { + } else if observable.is_power_observable() { + let x = doris + .doris_rx_power() + .filter_map(|(t_i, station_i, observable_i, _data)| { + if station_i == station && observable_i == observable { + Some(t_i) + } else { + None + } + }) + .collect::>(); + let y = doris + .doris_rx_power() + .filter_map(|(_t_i, station_i, observable_i, data)| { + if station_i == station && observable_i == observable { + Some(data) + } else { + None + } + }) + .collect::>(); + + let freq_label = Carrier::from_doris_observable(&observable) + .unwrap_or_else(|_| { + panic!("failed to determine plot freq_label for {}", observable) + }) + .to_string(); + + let trace = build_chart_epoch_axis( + &format!("{}({})", station.label, freq_label), + Mode::Markers, + x, + y, + ) + .marker(Marker::new().symbol(marker_symbol.clone())) + .visible({ + if station_index < 3 { + Visible::True + } else { + Visible::LegendOnly + } + }); + plot_ctx.add_trace(trace); + } else if observable.is_pseudorange_observable() { + let x = doris + .doris_pseudo_range() + .filter_map(|(t_i, station_i, observable_i, _data)| { + if station_i == station && observable_i == observable { + Some(t_i) + } else { + None + } + }) + .collect::>(); + let y = doris + .doris_pseudo_range() + .filter_map(|(_t_i, station_i, observable_i, data)| { + if station_i == station && observable_i == observable { + Some(data) + } else { + None + } + }) + .collect::>(); + + let freq_label = Carrier::from_doris_observable(&observable) + .unwrap_or_else(|_| { + panic!("failed to determine plot freq_label for {}", observable) + }) + .to_string(); + + let trace = build_chart_epoch_axis( + &format!("{}({})", station.label, freq_label), + Mode::Markers, + x, + y, + ) + .marker(Marker::new().symbol(marker_symbol.clone())) + .visible({ + if station_index < 3 { + Visible::True + } else { + Visible::LegendOnly + } + }); + plot_ctx.add_trace(trace); + } else if observable.is_phase_observable() { + let x = doris + .doris_phase() + .filter_map(|(t_i, station_i, observable_i, _data)| { + if station_i == station && observable_i == observable { + Some(t_i) + } else { + None + } + }) + .collect::>(); + let y = doris + .doris_phase() + .filter_map(|(_t_i, station_i, observable_i, data)| { + if station_i == station && observable_i == observable { + Some(data) + } else { + None + } + }) + .collect::>(); + + let freq_label = Carrier::from_doris_observable(&observable) + .unwrap_or_else(|_| { + panic!("failed to determine plot freq_label for {}", observable) + }) + .to_string(); + + let trace = build_chart_epoch_axis( + &format!("{}({})", station.label, freq_label), + Mode::Markers, + x, + y, + ) + .marker(Marker::new().symbol(marker_symbol.clone())) + .visible({ + if station_index < 3 { + Visible::True + } else { + Visible::LegendOnly + } + }); + plot_ctx.add_trace(trace); + } + } + } +} diff --git a/rinex-cli/src/graph/record/mod.rs b/rinex-cli/src/graph/record/mod.rs index 5f4af9700..e117f6063 100644 --- a/rinex-cli/src/graph/record/mod.rs +++ b/rinex-cli/src/graph/record/mod.rs @@ -1,3 +1,4 @@ +mod doris; mod ionex; mod ionosphere; mod meteo; @@ -5,6 +6,7 @@ mod navigation; mod observation; mod sp3_plot; +pub use doris::plot_doris_observations; pub use meteo::plot_meteo_observations; pub use navigation::plot_sv_nav_clock; pub use navigation::plot_sv_nav_orbits; diff --git a/rinex/src/doris/mod.rs b/rinex/src/doris/mod.rs index 506df9669..d0d4d76df 100644 --- a/rinex/src/doris/mod.rs +++ b/rinex/src/doris/mod.rs @@ -52,19 +52,19 @@ pub struct HeaderFields { } impl HeaderFields { - /// Retrieve station by ID# - pub(crate) fn get_station(&mut self, id: u16) -> Option<&Station> { - self.stations - .iter() - .filter(|s| s.key == id) - .reduce(|k, _| k) - } + // /// Retrieve station by ID# + // pub(crate) fn get_station(&mut self, id: u16) -> Option<&Station> { + // self.stations + // .iter() + // .filter(|s| s.key == id) + // .reduce(|k, _| k) + // } /// Insert a data scaling pub(crate) fn with_scaling(&mut self, observable: Observable, scaling: u16) { self.scaling.insert(observable.clone(), scaling); } - /// Returns scaling to applied to said Observable. - pub(crate) fn scaling(&self, observable: Observable) -> Option<&u16> { - self.scaling.get(&observable) - } + // /// Returns scaling to applied to said Observable. + // pub(crate) fn scaling(&self, observable: Observable) -> Option<&u16> { + // self.scaling.get(&observable) + // } } diff --git a/rinex/src/lib.rs b/rinex/src/lib.rs index d6f1fd0ea..4a9070f85 100644 --- a/rinex/src/lib.rs +++ b/rinex/src/lib.rs @@ -1522,49 +1522,67 @@ impl Rinex { })) } /// Returns a (unique) Iterator over all identified [`Observable`]s. - /// This will panic if invoked on other than OBS and Meteo RINEX. + /// Applies to Observation RINEX: + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/CRNX/V1/AJAC3550.21D") + /// .unwrap(); + /// for observable in rinex.observable() { + /// if observable.is_phase_observable() { + /// // do something + /// } + /// } + /// ``` + /// Also applies to Meteo RINEX: + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/MET/V2/abvi0010.15m") + /// .unwrap(); + /// for observable in rinex.observable() { + /// if *observable == Observable::Temperature { + /// // do something + /// } + /// } + /// ``` + /// Also applies to DORIS RINEX: + /// ``` + /// use rinex::prelude::*; + /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") + /// .unwrap(); + /// for observable in rinex.observable() { + /// if observable.is_pseudorange_observable() { + /// // do something + /// } + /// } + /// ``` pub fn observable(&self) -> Box + '_> { if self.record.as_obs().is_some() { Box::new( self.observation() - .map(|(_, (_, svnn))| { + .flat_map(|(_, (_, svnn))| { svnn.iter() - .flat_map(|(_sv, observables)| observables.keys()) + .flat_map(|(_, observables)| observables.iter().map(|(k, _)| k)) }) - .fold(vec![], |mut list, items| { - // create a unique list - for item in items { - if !list.contains(&item) { - list.push(item); - } - } - list - }) - .into_iter(), + .unique(), ) } else if self.record.as_meteo().is_some() { Box::new( self.meteo() - .map(|(_, observables)| { - observables.keys() - //.copied() - }) - .fold(vec![], |mut list, items| { - // create a unique list - for item in items { - if !list.contains(&item) { - list.push(item); - } - } - list + .flat_map(|(_, observables)| observables.iter().map(|(k, _)| k)) + .unique(), + ) + } else if self.record.as_doris().is_some() { + Box::new( + self.doris() + .flat_map(|(_, stations)| { + stations + .iter() + .flat_map(|(_, observables)| observables.iter().map(|(k, _)| k)) }) - .into_iter(), + .unique(), ) } else { - panic!( - ".observable() is not feasible on \"{:?}\" RINEX", - self.header.rinex_type - ); + Box::new([].iter()) } } /// Meteo RINEX record browsing method. Extracts data for this specific format. @@ -3545,10 +3563,10 @@ impl Rinex { /// use rinex::prelude::*; /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") /// .unwrap(); - /// for (epoch, station, value) in rinex.doris_moisture() { + /// for (epoch, station, value) in rinex.doris_humidity() { /// println!("{}@{}: {}%", station.domes, epoch, value); /// } - pub fn doris_moisture(&self) -> Box + '_> { + pub fn doris_humidity(&self) -> Box + '_> { Box::new(self.doris().flat_map(|((epoch, _), stations)| { stations.iter().flat_map(move |(station, observables)| { observables.iter().filter_map(move |(observable, data)| { @@ -3622,10 +3640,10 @@ impl Rinex { /// use rinex::prelude::*; /// let rinex = Rinex::from_file("../test_resources/DOR/V3/cs2rx18164.gz") /// .unwrap(); - /// for (epoch, station, code, value) in rinex.doris_power() { + /// for (epoch, station, code, value) in rinex.doris_rx_power() { /// println!("{} {}@{}: {} dBm", station.domes, code, epoch, value); /// } - pub fn doris_power( + pub fn doris_rx_power( &self, ) -> Box + '_> { Box::new(self.doris().flat_map(|((epoch, _), stations)| {