farm-ng-core
time_series.h
Go to the documentation of this file.
1 // Copyright 2022, farm-ng inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #pragma once
16 
18 #include <sophus/concepts/utils.h>
19 
20 #include <deque>
21 
22 namespace farm_ng {
23 
24 namespace time_series {
25 
26 /// Non-intrusive way to add getStamp function to a type needed for
27 /// StampedValue concept.
28 ///
29 /// The default implementation assumes a member variable called stamp of type
30 /// double.
31 template <class TType>
32 auto getStamp(TType const& value) -> double {
33  return value.stamp;
34 }
35 
36 /// StampedValue is a type that has a timestamp.
37 template <class TType>
38 concept StampedValue = requires(TType m) {
39  { getStamp<TType>(m) } -> sophus::concepts::ConvertibleTo<double>;
40 };
41 
42 template <class TType>
43 auto interpolate(
44  TType const& foo_from_bar, TType const& foo_from_daz, double p = 0.5)
45  -> TType;
46 
47 /// Interpolative is a type which can be interpolated
48 template <class TType>
49 concept InterpolativeValue = StampedValue<TType> &&
50  requires(TType m, double p) {
51  { interpolate<TType>(m, m, p) } -> sophus::concepts::ConvertibleTo<TType>;
52 };
53 
54 // New type to specify maximal allowed time gap for interpolation.
55 struct MaxGap {
57 
58  double time_duration;
59 };
60 
61 // New type to specify nearness to consider two values near in timeseries (e.g.
62 // to skip interpolation calculation).
65 
66  double time_thr;
67 };
68 
69 } // namespace time_series
70 
71 /// TimeSeries is a container of StampedValue that are sorted by their
72 /// timestamp.
73 ///
74 /// Invariant: time_ordered_series_ is sorted by time_series::getStamp.
75 ///
76 /// Consequence: The invariant implies that we have a strict weak ordering
77 /// relation on the time stamps. Hence, it is not allowed to add
78 /// NAN timestamps to the time series.
79 ///
80 /// TODO: Add std::ranges support to the API once we fully move to c++20.
81 template <time_series::StampedValue TValueWithStamp>
82 class TimeSeries {
83  public:
84  using ValueWithStamp = TValueWithStamp;
85  using Container = std::deque<ValueWithStamp>;
86  using ConstIterator = typename Container::const_iterator;
87 
88  /// Returns true if there are no Values in series.
89  bool empty() const { return time_ordered_series_.empty(); }
90 
91  /// Adds ValueWithStamp to series.
92  ///
93  /// Precondition: stamp is not NAN.
94  void insert(ValueWithStamp const& value) {
95  double stamp = time_series::getStamp(value);
96  if (std::isnan(stamp)) {
97  FARM_WARN("Nan timestamp ignore");
98  return;
99  }
100  auto it = findSmallestUpperBound(stamp);
101 
102  time_ordered_series_.insert(it, value);
103  }
104 
105  /// Number of values in series.
106  size_t size() const { return time_ordered_series_.size(); }
107 
108  /// access ValueWithStamp by index
109  ///
110  /// Precondition: idx < size()
111  ValueWithStamp const& operator[](size_t idx) const {
112  return time_ordered_series_[idx];
113  }
114 
115  /// Find nearest value given ``stamp``.
116  ///
117  /// Precondition: stamp is not NAN.
118  ///
119  /// Returns error if times series is empty.
120  Expected<ValueWithStamp> findNearest(double query_stamp) const {
121  auto it_smallest_upper_bound = findSmallestUpperBound(query_stamp);
122  if (it_smallest_upper_bound == cbegin() &&
123  it_smallest_upper_bound == cend()) {
124  return FARM_UNEXPECTED("Time series is empty.");
125  }
126 
127  if (it_smallest_upper_bound == cbegin()) {
128  // There is no element with stamp < query_stamp, return the smallest upper
129  // bound.
130  return *it_smallest_upper_bound;
131  }
132  auto it_greatest_lower_bound = std::prev(it_smallest_upper_bound);
133  if (it_smallest_upper_bound == cend()) {
134  // There is no element with stamp > query_stamp, return the greatest lower
135  // bound.
136  return *it_greatest_lower_bound;
137  }
138 
139  double lb_abs_diff =
140  std::abs(time_series::getStamp(*it_smallest_upper_bound) - query_stamp);
141  double ub_abs_diff =
142  std::abs(time_series::getStamp(*it_greatest_lower_bound) - query_stamp);
143 
144  return lb_abs_diff < ub_abs_diff ? *it_smallest_upper_bound
145  : *it_greatest_lower_bound;
146  }
147 
148  struct Bounds {
151  };
152 
153  Expected<Bounds> findBounds(double query_stamp) const {
154  auto it_smallest_upper_bound = findSmallestUpperBound(query_stamp);
155  if (it_smallest_upper_bound == cbegin() &&
156  it_smallest_upper_bound == cend()) {
157  return FARM_UNEXPECTED("Time series is empty.");
158  }
159 
160  if (it_smallest_upper_bound == cbegin()) {
161  return FARM_UNEXPECTED(
162  "No element with stamp < query_stamp ({}). Min/Max {}/{}",
163  query_stamp,
166  }
167  auto it_greatest_lower_bound = std::prev(it_smallest_upper_bound);
168  if (it_smallest_upper_bound == cend()) {
169  return FARM_UNEXPECTED(
170  "No element with stamp > query_stamp ({}). Min/Max {}/{}",
171  query_stamp,
174  }
175 
176  return Bounds{
177  .largest_lower_bound = *it_greatest_lower_bound,
178  .smallest_upper_bound = *it_smallest_upper_bound};
179  }
180 
181  /// Find nearest value given ``stamp`` within search radius ``threshold``.
182  ///
183  /// Precondition: stamp is not NAN.
184  ///
185  /// Returns error if |value.stamp - stamp| > threshold.
187  double stamp, double threshold) const {
188  FARM_TRY(auto, nearest_value, findNearest(stamp));
189  double abs_diff = std::abs(time_series::getStamp(nearest_value) - stamp);
190  if (abs_diff > threshold) {
191  return FARM_UNEXPECTED(
192  "|value.stamp ({}) - stamp ({})| ({}) > threshold ({})",
193  time_series::getStamp(nearest_value),
194  stamp,
195  abs_diff,
196  threshold);
197  }
198  return nearest_value;
199  }
200 
201  /// Just for unit testing.
202  ///
203  /// Must return always true.
204  bool testIsSortedInvariant() const {
205  return std::is_sorted(
206  time_ordered_series_.begin(),
207  time_ordered_series_.end(),
208  [](ValueWithStamp left, ValueWithStamp right) {
209  return time_series::getStamp(left) < time_series::getStamp(right);
210  });
211  }
212 
213  /// Returns interpolated value from time series at query_stamp.
214  ///
215  /// Note: Only available if TValueWithStamp meets
216  /// time_series::InterpolativeValue concept.
217  ///
218  /// The following strategy is used:
219  ///
220  /// 1. If query_stamp is close to an existing value in the series, using the
221  /// ``nearness_threshold``, that exiting value is return directly.
222  /// 2. Otherwise, an upper bound value and a lower bound value is found for
223  /// the query_stamp, and linearly interpolated result between the two
224  /// values is return, but only if ``|upper.t - lower.t| <=
225  /// maximal_time_gap``.
226  /// 3. If there is no upper or lower bound, or if the ``|upper.t - lower.t| >
227  /// maximal_time_gap`` and error is return.
228  std::enable_if_t<
229  time_series::InterpolativeValue<TValueWithStamp>,
232  double query_stamp,
233  time_series::MaxGap maximal_time_gap = time_series::MaxGap(1.0),
234  time_series::NearnessThreshold nearness_threshold =
235  time_series::NearnessThreshold(1e-5)) const {
236  auto expect_nearest =
237  this->findNearestWithin(query_stamp, nearness_threshold.time_thr);
238  if (expect_nearest) {
239  return FARM_UNWRAP(expect_nearest);
240  }
241 
242  return this->interpolatedValueImpl(
243  query_stamp, maximal_time_gap.time_duration);
244  }
245 
246  ValueWithStamp const& front() const { return time_ordered_series_.front(); }
247 
248  ValueWithStamp const& back() const { return time_ordered_series_.back(); }
249 
250  void clear() { return time_ordered_series_.clear(); }
251 
252  void pop_front() { return time_ordered_series_.pop_front(); }
253 
254  void pop_back() { return time_ordered_series_.pop_back(); }
255 
256  // begin of series.
257  ConstIterator begin() const { return time_ordered_series_.begin(); }
258  ConstIterator cbegin() const { return time_ordered_series_.cbegin(); }
259 
260  // end of series.
261  ConstIterator end() const { return time_ordered_series_.end(); }
262  ConstIterator cend() const { return time_ordered_series_.cend(); }
263 
264  private:
265  ConstIterator findSmallestUpperBound(double stamp) const {
266  FARM_ASSERT(!std::isnan(stamp));
267 
268  // "Returns an iterator pointing to the first element in the range [first,
269  // last) that does not satisfy element < value"
270  return std::lower_bound(
271  time_ordered_series_.begin(),
272  time_ordered_series_.end(),
273  stamp,
274  [](ValueWithStamp const& lhs, double rhs) {
275  return time_series::getStamp<ValueWithStamp>(lhs) < rhs;
276  });
277  }
278 
279  std::enable_if_t<
280  time_series::InterpolativeValue<ValueWithStamp>,
281  Expected<ValueWithStamp>>
282  interpolatedValueImpl(double timestamp, double max_delta_t = 1.0) const {
283  FARM_TRY(auto, bounds, this->findBounds(timestamp));
284 
285  auto lower = bounds.largest_lower_bound;
286  auto upper = bounds.smallest_upper_bound;
287  double lower_stamp = time_series::getStamp(lower);
288  double upper_stamp = time_series::getStamp(upper);
289 
290  FARM_ASSERT_LE(lower_stamp, upper_stamp);
291  double delta_t = std::abs(lower_stamp - upper_stamp);
292  // negated comparisons to also catch NANSs
293  if (!(delta_t < max_delta_t)) {
294  return FARM_UNEXPECTED(
295  "Delta of [{}, {}] = {}, which is too large (> {})",
296  lower_stamp,
297  upper_stamp,
298  delta_t,
299  max_delta_t);
300  }
301 
302  // interpolation factor where p=0 means 100% of lower, and p=1.0 means
303  // 100% of upper.
304  double p = 1.0 - (upper_stamp - timestamp) / delta_t;
305  FARM_ASSERT(p >= 0.0 && p <= 1.0, "p ({}) must in [0, 1].", p);
306 
307  // "proof" by example:
308  //
309  // case 1:
310  // lower_stamp = timestamp
311  // upper_stamp = timestamp + 0.5
312  // hence, delta_t = 0.5
313  //
314  // p = 1.0 - (upper-timestamp)/0.5 = 1.0 - 0.5/0.5 = 0.0
315  //
316  // case 2:
317  // lower_stamp = timestamp - 2.0
318  // upper_stamp = timestamp
319  // hence, delta_t = 2.0
320  //
321  // p = 1.0 - (upper-timestamp)/2.0 = 1.0 - 0.0/2.0 = 1.0
322 
323  return time_series::interpolate(lower, upper, p);
324  }
325 
326  // Invariant: sorted, see details above.
327  Container time_ordered_series_;
328 };
329 
330 } // namespace farm_ng
farm_ng::time_series::MaxGap
Definition: time_series.h:55
farm_ng
Definition: backtrace.cpp:102
farm_ng::TimeSeries::clear
void clear()
Definition: time_series.h:250
farm_ng::time_series::NearnessThreshold::time_thr
double time_thr
Definition: time_series.h:66
farm_ng::time_series::getStamp
auto getStamp(TType const &value) -> double
Non-intrusive way to add getStamp function to a type needed for StampedValue concept.
Definition: time_series.h:32
FARM_TRY
#define FARM_TRY(Type, var, expression)
Assigns *expression to var of Type, but returns error if there is one.
Definition: expected.h:91
farm_ng::TimeSeries::Bounds::smallest_upper_bound
ValueWithStamp smallest_upper_bound
Definition: time_series.h:150
farm_ng::TimeSeries::findNearest
Expected< ValueWithStamp > findNearest(double query_stamp) const
Find nearest value given stamp.
Definition: time_series.h:120
FARM_ASSERT
#define FARM_ASSERT(condition,...)
If condition is false, Print formatted error message and then panic.
Definition: logger.h:264
farm_ng::TimeSeries::Bounds::largest_lower_bound
ValueWithStamp largest_lower_bound
Definition: time_series.h:149
farm_ng::TimeSeries::empty
bool empty() const
Returns true if there are no Values in series.
Definition: time_series.h:89
farm_ng::time_series::NearnessThreshold::NearnessThreshold
NearnessThreshold(double time_thr)
Definition: time_series.h:64
farm_ng::TimeSeries::insert
void insert(ValueWithStamp const &value)
Adds ValueWithStamp to series.
Definition: time_series.h:94
farm_ng::TimeSeries::interpolatedValue
std::enable_if_t< time_series::InterpolativeValue< TValueWithStamp >, Expected< TValueWithStamp > > interpolatedValue(double query_stamp, time_series::MaxGap maximal_time_gap=time_series::MaxGap(1.0), time_series::NearnessThreshold nearness_threshold=time_series::NearnessThreshold(1e-5)) const
Returns interpolated value from time series at query_stamp.
Definition: time_series.h:231
farm_ng::time_series::NearnessThreshold
Definition: time_series.h:63
farm_ng::TimeSeries::end
ConstIterator end() const
Definition: time_series.h:261
farm_ng::TimeSeries
TimeSeries is a container of StampedValue that are sorted by their timestamp.
Definition: time_series.h:82
expected.h
farm_ng::time_series::MaxGap::time_duration
double time_duration
Definition: time_series.h:58
farm_ng::TimeSeries::Bounds
Definition: time_series.h:148
FARM_WARN
#define FARM_WARN(...)
More significant information with high signal to noise. Typically to communicate an usual but valid s...
Definition: logger.h:161
utils.h
farm_ng::TimeSeries::testIsSortedInvariant
bool testIsSortedInvariant() const
Just for unit testing.
Definition: time_series.h:204
farm_ng::TimeSeries::size
size_t size() const
Number of values in series.
Definition: time_series.h:106
FARM_ASSERT_LE
#define FARM_ASSERT_LE(lhs, rhs,...)
If it is false that lhs <= rhs, print formatted error message and then panic.
Definition: logger.h:289
farm_ng::TimeSeries::back
ValueWithStamp const & back() const
Definition: time_series.h:248
farm_ng::TimeSeries::ValueWithStamp
TValueWithStamp ValueWithStamp
Definition: time_series.h:84
farm_ng::TimeSeries::findBounds
Expected< Bounds > findBounds(double query_stamp) const
Definition: time_series.h:153
FARM_UNEXPECTED
#define FARM_UNEXPECTED(cstr,...)
Definition: expected.h:86
farm_ng::time_series::interpolate
auto interpolate(TType const &foo_from_bar, TType const &foo_from_daz, double p=0.5) -> TType
farm_ng::TimeSeries::cend
ConstIterator cend() const
Definition: time_series.h:262
farm_ng::time_series::StampedValue
concept StampedValue
StampedValue is a type that has a timestamp.
Definition: time_series.h:38
farm_ng::TimeSeries::findNearestWithin
Expected< ValueWithStamp > findNearestWithin(double stamp, double threshold) const
Find nearest value given stamp within search radius threshold.
Definition: time_series.h:186
farm_ng::TimeSeries::Container
std::deque< ValueWithStamp > Container
Definition: time_series.h:85
farm_ng::TimeSeries::operator[]
ValueWithStamp const & operator[](size_t idx) const
access ValueWithStamp by index
Definition: time_series.h:111
farm_ng::TimeSeries::pop_front
void pop_front()
Definition: time_series.h:252
farm_ng::TimeSeries::begin
ConstIterator begin() const
Definition: time_series.h:257
farm_ng::time_series::MaxGap::MaxGap
MaxGap(double time_duration)
Definition: time_series.h:56
farm_ng::TimeSeries::ConstIterator
typename Container::const_iterator ConstIterator
Definition: time_series.h:86
farm_ng::TimeSeries::front
ValueWithStamp const & front() const
Definition: time_series.h:246
farm_ng::Expected
tl::expected< TT, TE > Expected
Definition: expected.h:37
FARM_UNWRAP
#define FARM_UNWRAP(wrapper,...)
Returns *wrapper, but panics if wrapper is nullopt or null.
Definition: logger.h:576
farm_ng::TimeSeries::pop_back
void pop_back()
Definition: time_series.h:254
farm_ng::time_series::InterpolativeValue
concept InterpolativeValue
Interpolative is a type which can be interpolated.
Definition: time_series.h:49
farm_ng::TimeSeries::cbegin
ConstIterator cbegin() const
Definition: time_series.h:258