1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
use crate::prelude::{Duration, Epoch, Rinex, TimeSeries};
impl Rinex {
/// Returns first [Epoch] encountered in time
pub fn first_epoch(&self) -> Option<Epoch> {
self.epoch_iter().next()
}
/// Returns last [Epoch] encountered in time
pub fn last_epoch(&self) -> Option<Epoch> {
self.epoch_iter().last()
}
/// Returns total [Duration] this [Rinex].
pub fn duration(&self) -> Option<Duration> {
let start = self.first_epoch()?;
let end = self.last_epoch()?;
Some(end - start)
}
/// Form a [`Timeseries`] iterator spanning [Self::duration]
/// with [Self::dominant_sample_rate] spacing
pub fn timeseries(&self) -> Option<TimeSeries> {
let start = self.first_epoch()?;
let end = self.last_epoch()?;
let dt = self.dominant_sampling_interval()?;
Some(TimeSeries::inclusive(start, end, dt))
}
/// Returns sample rate report by the GNSS receiver (if any).
/// NB: this is not actual data set analysis.
pub fn sampling_interval(&self) -> Option<Duration> {
self.header.sampling_interval
}
/// Returns dominant sampling period, expressed as [Duration], by actual data analysis.
/// ```
/// use rinex::prelude::*;
/// let rnx = Rinex::from_file("data/MET/V2/abvi0010.15m")
/// .unwrap();
/// assert_eq!(
/// rnx.dominant_sampling_interval(),
/// Some(Duration::from_seconds(60.0)));
/// ```
pub fn dominant_sampling_interval(&self) -> Option<Duration> {
self.sampling_histogram()
.max_by(|(_, pop_i), (_, pop_j)| pop_i.cmp(pop_j))
.map(|dominant| dominant.0)
}
/// Returns dominant sample rate (in Hertz) by actual data analysis.
pub fn dominant_sampling_rate_hz(&self) -> Option<f64> {
let interval = self.dominant_sampling_interval()?;
Some(1.0 / interval.to_seconds())
}
/// Histogram analysis on Epoch interval. Although
/// it is feasible on all types indexed by [Epoch],
/// this operation only makes truly sense on Observation Data.
/// ```
/// use rinex::prelude::*;
/// use itertools::Itertools;
/// use std::collections::HashMap;
/// let rinex = Rinex::from_file("data/OBS/V2/AJAC3550.21O")
/// .unwrap();
/// assert!(
/// rinex.sampling_histogram().sorted().eq(vec![
/// (Duration::from_seconds(30.0), 1),
/// ]),
/// "sampling_histogram failed"
/// );
/// ```
pub fn sampling_histogram(&self) -> Box<dyn Iterator<Item = (Duration, usize)> + '_> {
// compute dt = |e_k+1 - e_k| : instantaneous epoch delta
// then compute an histogram on these intervals
Box::new(
self.epoch_iter()
.zip(self.epoch_iter().skip(1))
.map(|(ek, ekp1)| ekp1 - ek) // following step computes the histogram
// and at the same time performs a .unique() like filter
.fold(vec![], |mut list, dt| {
let mut found = false;
for (delta, pop) in list.iter_mut() {
if *delta == dt {
*pop += 1;
found = true;
break;
}
}
if !found {
list.push((dt, 1));
}
list
})
.into_iter(),
)
}
/// Returns True if Self has a steady sampling, ie., all epoch interval
/// are evenly spaced
pub fn steady_sampling(&self) -> bool {
self.sampling_histogram().count() == 1
}
/// Returns an iterator over unexpected data gaps,
/// in the form ([`Epoch`], [`Duration`]), where
/// epoch is the starting datetime, and its related duration.
/// ```
/// use std::str::FromStr;
/// use rinex::prelude::{Rinex, Epoch, Duration};
/// let rinex = Rinex::from_file("data/MET/V2/abvi0010.15m")
/// .unwrap();
///
/// // when tolerance is set to None,
/// // the reference sample rate is [Self::dominant_sample_rate].
/// let mut tolerance : Option<Duration> = None;
/// let gaps : Vec<_> = rinex.data_gaps(tolerance).collect();
/// assert!(
/// rinex.data_gaps(None).eq(
/// vec![
/// (Epoch::from_str("2015-01-01T00:09:00 UTC").unwrap(), Duration::from_seconds(8.0 * 3600.0 + 51.0 * 60.0)),
/// (Epoch::from_str("2015-01-01T09:04:00 UTC").unwrap(), Duration::from_seconds(10.0 * 3600.0 + 21.0 * 60.0)),
/// (Epoch::from_str("2015-01-01T19:54:00 UTC").unwrap(), Duration::from_seconds(3.0 * 3600.0 + 1.0 * 60.0)),
/// (Epoch::from_str("2015-01-01T23:02:00 UTC").unwrap(), Duration::from_seconds(7.0 * 60.0)),
/// (Epoch::from_str("2015-01-01T23:21:00 UTC").unwrap(), Duration::from_seconds(31.0 * 60.0)),
/// ]),
/// "data_gaps(tol=None) failed"
/// );
///
/// // with a tolerance, we tolerate the given gap duration
/// tolerance = Some(Duration::from_seconds(3600.0));
/// let gaps : Vec<_> = rinex.data_gaps(tolerance).collect();
/// assert!(
/// rinex.data_gaps(Some(Duration::from_seconds(3.0 * 3600.0))).eq(
/// vec![
/// (Epoch::from_str("2015-01-01T00:09:00 UTC").unwrap(), Duration::from_seconds(8.0 * 3600.0 + 51.0 * 60.0)),
/// (Epoch::from_str("2015-01-01T09:04:00 UTC").unwrap(), Duration::from_seconds(10.0 * 3600.0 + 21.0 * 60.0)),
/// (Epoch::from_str("2015-01-01T19:54:00 UTC").unwrap(), Duration::from_seconds(3.0 * 3600.0 + 1.0 * 60.0)),
/// ]),
/// "data_gaps(tol=3h) failed"
/// );
/// ```
pub fn data_gaps(
&self,
tolerance: Option<Duration>,
) -> Box<dyn Iterator<Item = (Epoch, Duration)> + '_> {
let sample_rate: Duration = match tolerance {
Some(dt) => dt, // user defined
None => {
match self.dominant_sampling_interval() {
Some(dt) => dt,
None => {
match self.sampling_interval() {
Some(dt) => dt,
None => {
// not enough information
// this is probably not an Epoch iterated RINEX
return Box::new(Vec::<(Epoch, Duration)>::new().into_iter());
},
}
},
}
},
};
Box::new(
self.epoch_iter()
.zip(self.epoch_iter().skip(1))
.filter_map(move |(ek, ekp1)| {
let dt = ekp1 - ek; // gap
if dt > sample_rate {
// too large
Some((ek, dt)) // retain starting datetime and gap duration
} else {
None
}
}),
)
}
}
#[cfg(test)]
#[cfg(feature = "flate2")]
mod test {
use crate::prelude::Rinex;
#[test]
fn glacier_20240506_dominant_sample_rate() {
let rnx = Rinex::from_gzip_file(format!(
"{}/data/OBS/V3/240506_glacier_station.obs.gz",
env!("CARGO_MANIFEST_DIR")
))
.unwrap();
let sampling_rate_hz = rnx.dominant_sampling_rate_hz().unwrap();
assert_eq!(sampling_rate_hz, 1.0);
}
}