SatCat5
ptp_filters.h
Go to the documentation of this file.
1 // Copyright 2024 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
27 
28 #pragma once
29 
30 #include <satcat5/list.h>
31 #include <satcat5/ptp_time.h>
32 #include <satcat5/wide_integer.h>
33 
34 namespace satcat5 {
35  namespace ptp {
38  class Filter {
39  public:
42  virtual void reset() = 0;
43 
48  virtual void rate(s64 delta_subns, u32 elapsed_usec) {}
49 
55  virtual s64 update(s64 next, u32 elapsed_usec) = 0;
56 
57  protected:
59  constexpr Filter() : m_next(0) {}
60  ~Filter() {}
61 
62  private:
63  // Linked-list of chained filter objects.
65  satcat5::ptp::Filter* m_next;
66  };
67 
70  class DebugFilter final : public satcat5::ptp::Filter {
71  public:
72  constexpr DebugFilter()
73  : m_prev(0), m_usec(0) {}
74 
76  void reset() override
77  { m_prev = 0; m_usec = 0; }
78 
80  s64 update(s64 next, u32 elapsed_usec) override
81  { m_prev = next; m_usec = elapsed_usec; return next; }
82 
85  inline s64 prev() const { return m_prev; }
86  inline u32 usec() const { return m_usec; }
88 
89  protected:
90  s64 m_prev;
91  u32 m_usec;
92  };
93 
95  template<typename T, unsigned MAX_WINDOW>
96  class SlidingWindow final {
97  public:
98  SlidingWindow() : m_wridx(0), m_window{0} {}
99  ~SlidingWindow() {}
100 
101  // Copy the N most recent samples from the working buffer.
102  // (The last index of the output array is the most recent sample.)
103  void read(T* dst, unsigned count) const {
104  if (m_wridx >= count) {
105  memcpy(dst, m_window + m_wridx - count, count*sizeof(T));
106  } else {
107  unsigned wrap = count - m_wridx;
108  memcpy(dst, m_window + MAX_WINDOW - wrap, wrap*sizeof(T));
109  memcpy(dst + wrap, m_window, m_wridx*sizeof(T));
110  }
111  }
112 
113  // Write one new sample to the working buffer.
114  void push(const T& next) {
115  m_window[m_wridx] = next;
116  if (++m_wridx >= MAX_WINDOW) m_wridx = 0;
117  }
118 
119  void reset() {
120  memset(m_window, 0, MAX_WINDOW * sizeof(T));
121  }
122 
123  protected:
124  unsigned m_wridx;
125  T m_window[MAX_WINDOW];
126  };
127 
128  // Low-level functions implemented outside the template classes.
129  s64 boxcar_filter(const s64* data, unsigned order);
130  s64 median_filter(s64* data, unsigned samps);
131 
136  public:
137  explicit AmplitudeReject(unsigned tau_msec = 10000);
138 
139  // Accessors for settings and internal state.
140  inline s64 get_mean() const {return m_mean;}
141  inline u64 get_sigma() const {return m_sigma;}
142  inline void set_min(u64 min_subns) {m_min = min_subns;}
143  inline void set_tau(u32 tau_msec) {m_tau_usec = 1000*tau_msec;}
144 
145  // Required API from ptp::Filter.
146  void reset() override;
147  s64 update(s64 next, u32 elapsed_usec) override;
148 
149  protected:
150  s64 m_mean;
151  u64 m_sigma;
152  u64 m_min;
153  u32 m_tau_usec;
154  };
155 
158  template<unsigned MAX_ORDER>
160  public:
161  explicit BoxcarFilter(unsigned order = MAX_ORDER)
162  : m_order(0) {set_order(order);}
163 
164  inline void set_order(unsigned x)
165  { if (x <= MAX_ORDER) m_order = x; }
166 
167  void reset() override
168  { m_window.reset(); }
169 
170  s64 update(s64 next, u32 elapsed_usec) override {
171  if (next == INT64_MAX) return INT64_MAX;
172  s64 temp[MAX_WINDOW];
173  m_window.push(next);
174  m_window.read(temp, 1u << m_order);
175  return satcat5::ptp::boxcar_filter(temp, m_order);
176  }
177 
178  protected:
179  static constexpr unsigned MAX_WINDOW = 1u << MAX_ORDER;
180  unsigned m_order;
182  };
183 
186  template<unsigned MAX_ORDER>
188  public:
189  explicit MedianFilter(unsigned order = MAX_ORDER)
190  : m_order(0) {set_order(order);}
191 
192  inline void set_order(unsigned x)
193  { if (x <= MAX_ORDER) m_order = x|1; }
194 
195  void reset() override
196  { m_window.reset(); }
197 
198  s64 update(s64 next, u32 elapsed_usec) override {
199  if (next == INT64_MAX) return INT64_MAX;
200  s64 temp[MAX_ORDER];
201  m_window.push(next);
202  m_window.read(temp, m_order);
203  return satcat5::ptp::median_filter(temp, m_order);
204  }
205 
206  protected:
207  unsigned m_order;
209  };
210 
224  struct CoeffPI {
225  public:
227  explicit constexpr CoeffPI(double tau_secs)
228  : kp(satcat5::util::round_u64z(k1(tau_secs, 0.707) / fw_gain()))
229  , ki(satcat5::util::round_u64z(k2(tau_secs, 0.707) / fw_gain()))
230  {} // No other initialization required.
231  CoeffPI(const CoeffPI& t) = default;
232  CoeffPI& operator=(const CoeffPI& t) = default;
233 
235  bool ok() const {return (kp > 7) && (ki > 7);}
236 
239  static constexpr unsigned SCALE = 60;
240 
241  protected:
242  // Calculate alpha2, K1, and K2 from Stephens & Thomas Table II.
243  // Note: Omit scaling by T0; compensate for this at runtime.
244  static constexpr double alpha(double zeta)
245  { return 0.25 / (zeta * zeta); }
246  static constexpr double k1(double tau, double zeta)
247  { return 1.273239545 / (tau * (1.0 + alpha(zeta))); }
248  static constexpr double k2(double tau, double zeta)
249  { return alpha(zeta) * k1(tau, zeta) * k1(tau, zeta); }
250  // End-to-end loop gain including intermediate scaling:
251  // * T0 compensation: Multiply by assumed T0 = 1 sec.
252  // * Cycles to radians: Effective gain = 1 / (2*pi).
253  // * Output scaling: Divide final output by 2^SCALE.
254  static constexpr double fw_gain()
255  { return double(satcat5::ptp::USEC_PER_SEC)
256  / 6.28318530717958647693
257  / satcat5::util::pow2d(SCALE); }
258 
260  u64 kp; // Proportional coefficient (LSB per subns)
261  u64 ki; // Integral coefficient (LSB per subns)
262  };
263 
268  public:
270  explicit ControllerPI(const satcat5::ptp::CoeffPI& coeff);
271 
273  void set_coeff(const satcat5::ptp::CoeffPI& coeff);
274 
276  inline void set_slew(u64 slew) { m_slew = slew; }
277 
280  void reset() override;
281  void rate(s64 delta, u32 elapsed_usec) override;
282  s64 update(s64 next, u32 elapsed_usec) override;
284 
285  protected:
286  // Internal state.
287  satcat5::ptp::CoeffPI m_coeff;
288  satcat5::util::int128_t m_accum;
289  u64 m_slew;
290  };
291 
295  struct CoeffPII {
296  public:
298  explicit constexpr CoeffPII(double tau_secs)
299  : kp(satcat5::util::round_u64z(k1(tau_secs) / fw_gain()))
300  , ki(satcat5::util::round_u64z(k2(tau_secs) / fw_gain()))
301  , kr(satcat5::util::round_u64z(kratio(tau_secs)))
302  {} // No other initialization required.
303  CoeffPII(const CoeffPII& t) = default;
304  CoeffPII& operator=(const CoeffPII& t) = default;
305 
307  bool ok() const {return (kp > 7) && (ki > 7) && (kr > 7);}
308 
312  static constexpr unsigned SCALE1 = 70;
313  static constexpr unsigned SCALE2 = 64;
314  static constexpr unsigned SCALE = SCALE1 + SCALE2;
316 
317  protected:
318  // "Standard underdamped" K1, K2, and K2 from Stephens & Thomas Table III.
319  // Note: Omit scaling by T0; compensate for this at runtime.
320  static constexpr double k1(double tau)
321  { return 0.830373616 / tau; } // i.e., 60 / 23pi
322  static constexpr double k2(double tau)
323  { return (4.0/9.0) * k1(tau) * k1(tau); }
324  static constexpr double k3(double tau)
325  { return (2.0/27.0) * k1(tau) * k1(tau) * k1(tau); }
326  // Ratio of K3 / K2, used for nested-accumulator updates.
327  static constexpr double kratio(double tau)
328  { return k3(tau) / k2(tau)
329  * satcat5::util::pow2d(SCALE2)
330  / double(satcat5::ptp::USEC_PER_SEC); }
331  // End-to-end loop gain for including intermediate scaling:
332  // * T0 compensation: Multiply by assumed T0 = 1 sec.
333  // * Cycles to radians: Effective gain = 1 / (2*pi).
334  // * Output scaling: Divide final output by 2^SCALE1.
335  static constexpr double fw_gain()
336  { return double(satcat5::ptp::USEC_PER_SEC)
337  / 6.28318530717958647693
339 
341  u64 kp; // Proportional coefficient (LSB per subns)
342  u64 ki; // Integral coefficient (LSB per subns)
343  u64 kr; // Double-integral coefficient (K3 / K2)
344  };
345 
350  public:
352  explicit ControllerPII(const satcat5::ptp::CoeffPII& coeff);
353 
355  void set_coeff(const satcat5::ptp::CoeffPII& coeff);
356 
358  inline void set_slew(u64 slew) { m_slew = slew; }
359 
362  void reset() override;
363  void rate(s64 delta, u32 elapsed_usec) override;
364  s64 update(s64 next, u32 elapsed_usec) override;
366 
367  protected:
368  // Internal state.
369  satcat5::ptp::CoeffPII m_coeff;
370  satcat5::util::int128_t m_accum1;
371  satcat5::util::int256_t m_accum2;
372  u64 m_slew;
373  };
374 
379  static constexpr unsigned TSCALE = 48;
380  satcat5::util::int128_t alpha; // Intercept at x = 0
381  satcat5::util::int128_t beta; // Slope * 2^TSCALE
383 
385  constexpr LinearRegression()
386  : alpha(satcat5::util::INT128_ZERO)
387  , beta(satcat5::util::INT128_ZERO) {}
388  LinearRegression(const LinearRegression& t) = default;
389  LinearRegression& operator=(const LinearRegression& t) = default;
390 
392  LinearRegression(const unsigned n, const s64* x, const s64* y);
393 
395  s64 extrapolate(s64 x) const;
396  };
397 
400  struct CoeffLR {
401  public:
403  explicit constexpr CoeffLR(double tau_secs)
404  : ki(satcat5::util::round_u64z(ki_gain() / tau_secs))
405  , kw(satcat5::util::round_u64z(kw_gain() * 2.0 / tau_secs))
406  {} // No other initialization required.
407  CoeffLR(const CoeffLR& t) = default;
408  CoeffLR& operator=(const CoeffLR& t) = default;
409 
411  bool ok() const {return (ki > 7) && (kw > 7);}
412 
413  protected:
414  static constexpr double ki_gain()
415  { return double(satcat5::ptp::USEC_PER_SEC); }
416  static constexpr double kw_gain()
417  { return satcat5::util::pow2d(LinearRegression::TSCALE)
418  / double(satcat5::ptp::USEC_PER_SEC); }
419 
421  u64 ki; // Integral coefficient (LSB per subns)
422  u64 kw; // Intercept scaling factor (LSB per usec)
423  };
424 
428  public:
430  void set_coeff(const satcat5::ptp::CoeffLR& coeff);
431 
433  void rate(s64 delta, u32 elapsed_usec) override;
434 
435  protected:
437  ControllerLR_Inner(const satcat5::ptp::CoeffLR& coeff, unsigned window);
438  ~ControllerLR_Inner() {}
439 
440  // Signal processing functions.
441  s64 update_inner(const u32* dt, const s64* y);
442 
443  // Internal state, not including sliding-window buffers.
444  satcat5::ptp::CoeffLR m_coeff;
445  satcat5::util::int128_t m_accum;
446  unsigned m_window;
447  };
448 
453  template<unsigned MAX_WINDOW>
455  public:
457  explicit ControllerLR(const satcat5::ptp::CoeffLR& coeff)
458  : satcat5::ptp::ControllerLR_Inner(coeff, MAX_WINDOW)
459  , m_count(0), m_elapsed(0) {}
460  static_assert(MAX_WINDOW >= 2, "MAX_WINDOW must be at least 2.");
461 
464  void set_window(unsigned window) {
465  if (2 <= window && window <= MAX_WINDOW) m_window = window;
466  }
467 
469  void reset() override {
470  m_dly.reset();
471  m_dat.reset();
472  m_accum = satcat5::util::INT128_ZERO;
473  }
474 
476  s64 update(s64 next, u32 elapsed_usec) override {
477  // Push valid samples into the sliding-window buffers.
478  // (Elapsed time still increments even if we drop a sample.)
479  m_elapsed += elapsed_usec;
480  if (next == INT64_MAX) return INT64_MAX;
481  m_dly.push(m_elapsed);
482  m_dat.push(next);
483  m_elapsed = 0;
484  // Attempt to read a full window of samples...
485  if (m_count < MAX_WINDOW) ++m_count;
486  if (m_count < m_window) return INT64_MAX;
487  u32 temp_dly[MAX_WINDOW]; m_dly.read(temp_dly, m_window);
488  s64 temp_dat[MAX_WINDOW]; m_dat.read(temp_dat, m_window);
489  // Proceed with linear-regression processing.
490  return update_inner(temp_dly, temp_dat);
491  }
492 
493  protected:
494  unsigned m_count;
495  u32 m_elapsed;
498  };
499 
506  public:
510  explicit constexpr LinearPrediction(satcat5::ptp::Filter* ctrl = 0)
511  : m_filters(ctrl), m_first(true), m_rate(0)
512  , m_accum(satcat5::util::INT128_ZERO) {}
513 
516  inline void add_filter(satcat5::ptp::Filter* filter)
517  { m_filters.push_back(filter); }
518 
521  void reset() override;
522  void rate(s64 delta, u32 elapsed_usec) override;
523  s64 update(s64 next, u32 elapsed_usec) override;
525 
527  s64 predict(u32 elapsed_usec) const;
528 
529  protected:
530  // Convert normalized rate to match accumulator scale.
531  static constexpr unsigned SCALE = 32;
532  satcat5::util::int128_t incr(u32 elapsed_usec) const;
533 
534  // Internal state.
536  bool m_first;
537  s64 m_rate;
538  satcat5::util::int128_t m_accum;
539  };
540 
546  public:
549  constexpr RateConversion(double ref_clk_hz, unsigned scale_ns)
550  : m_scale(satcat5::util::round_s64z(fw_gain(scale_ns) / ref_clk_hz)) {}
551 
553  bool ok() const {return satcat5::util::abs_s64(m_scale) > 1000000;}
554 
556  s64 convert(s64 offset) const;
557 
559  s64 invert(s64 rate) const;
560 
561  protected:
562  // Internal scaling is optimized for 1-200 MHz clocks.
563  static constexpr unsigned SHIFT = 48;
564  static constexpr double fw_gain(unsigned scale_ns) {
565  return satcat5::util::pow2d(scale_ns + SHIFT)
566  / double(satcat5::ptp::SUBNS_PER_NSEC);
567  }
568  s64 m_scale;
569  };
570  }
571 }
Amplitude-based outlier rejection.
Definition: ptp_filters.h:135
void reset() override
Flush previous inputs and reset to a neutral state.
Definition: ptp_filters.cc:141
s64 update(s64 next, u32 elapsed_usec) override
Method called for each new input sample.
Definition: ptp_filters.cc:146
An FIR low-pass filter using "boxcar" averaging over 2^N samples.
Definition: ptp_filters.h:159
s64 update(s64 next, u32 elapsed_usec) override
Method called for each new input sample.
Definition: ptp_filters.h:170
void reset() override
Flush previous inputs and reset to a neutral state.
Definition: ptp_filters.h:167
Helper class for "ControllerLR" is never used directly.
Definition: ptp_filters.h:427
void set_coeff(const satcat5::ptp::CoeffLR &coeff)
Adjust loop bandwidth.
Definition: ptp_filters.cc:384
ControllerLR_Inner(const satcat5::ptp::CoeffLR &coeff, unsigned window)
Private constructor and destructor.
Definition: ptp_filters.cc:378
void rate(s64 delta, u32 elapsed_usec) override
Partial API from ptp::Filter.
Definition: ptp_filters.cc:397
Loop-filter for a linear-regression (LR) controller.
Definition: ptp_filters.h:454
ControllerLR(const satcat5::ptp::CoeffLR &coeff)
Constructor sets loop bandwidth, which can be changed later.
Definition: ptp_filters.h:457
void reset() override
Remaining API from ptp::Filter.
Definition: ptp_filters.h:469
s64 update(s64 next, u32 elapsed_usec) override
Implement the required API from ptp::Filter.
Definition: ptp_filters.h:476
void set_window(unsigned window)
Adjust window-size.
Definition: ptp_filters.h:464
Loop-filter for a proportional-integral (PI) controller.
Definition: ptp_filters.h:267
void rate(s64 delta, u32 elapsed_usec) override
Required API from ptp::Filter.
Definition: ptp_filters.cc:211
s64 update(s64 next, u32 elapsed_usec) override
Required API from ptp::Filter.
Definition: ptp_filters.cc:222
void set_slew(u64 slew)
Adjust maximum slew-rate.
Definition: ptp_filters.h:276
ControllerPI(const satcat5::ptp::CoeffPI &coeff)
Constructor sets loop bandwidth, which can be changed later.
Definition: ptp_filters.cc:187
void reset() override
Required API from ptp::Filter.
Definition: ptp_filters.cc:207
void set_coeff(const satcat5::ptp::CoeffPI &coeff)
Adjust tracking-loop bandwidth.
Definition: ptp_filters.cc:195
Loop-filter for a proportional-double-integral (PII) controller.
Definition: ptp_filters.h:349
s64 update(s64 next, u32 elapsed_usec) override
Required API from ptp::Filter.
Definition: ptp_filters.cc:296
ControllerPII(const satcat5::ptp::CoeffPII &coeff)
Constructor sets loop bandwidth, which can be changed later.
Definition: ptp_filters.cc:258
void rate(s64 delta, u32 elapsed_usec) override
Required API from ptp::Filter.
Definition: ptp_filters.cc:285
void reset() override
Required API from ptp::Filter.
Definition: ptp_filters.cc:280
void set_slew(u64 slew)
Adjust maximum slew-rate.
Definition: ptp_filters.h:358
void set_coeff(const satcat5::ptp::CoeffPII &coeff)
Adjust tracking-loop bandwidth.
Definition: ptp_filters.cc:267
DebugFilter remembers the last received call to update.
Definition: ptp_filters.h:70
s64 prev() const
Accessors for received parameters.
Definition: ptp_filters.h:85
void reset() override
Reset history.
Definition: ptp_filters.h:76
u32 usec() const
Accessors for received parameters.
Definition: ptp_filters.h:86
s64 update(s64 next, u32 elapsed_usec) override
Simple passthrough, storing both parameters.
Definition: ptp_filters.h:80
Define the basic chain-of-filters API.
Definition: ptp_filters.h:38
virtual void rate(s64 delta_subns, u32 elapsed_usec)
Optional handler for fast-acquisition; override if required.
Definition: ptp_filters.h:48
constexpr Filter()
Private constructor and destructor.
Definition: ptp_filters.h:59
virtual void reset()=0
Flush previous inputs and reset to a neutral state.
virtual s64 update(s64 next, u32 elapsed_usec)=0
Method called for each new input sample.
An inline filter that iteratively estimates linear trends.
Definition: ptp_filters.h:505
constexpr LinearPrediction(satcat5::ptp::Filter *ctrl=0)
Create this object, and optionally add the first filter.
Definition: ptp_filters.h:510
s64 predict(u32 elapsed_usec) const
Extrapolate trendline relative to most recent update() event.
Definition: ptp_filters.cc:485
void rate(s64 delta, u32 elapsed_usec) override
Required API from ptp::Filter.
Definition: ptp_filters.cc:448
s64 update(s64 next, u32 elapsed_usec) override
Required API from ptp::Filter.
Definition: ptp_filters.cc:462
void add_filter(satcat5::ptp::Filter *filter)
Add to the chain of processing filters.
Definition: ptp_filters.h:516
void reset() override
Required API from ptp::Filter.
Definition: ptp_filters.cc:435
A median filter for an odd number of elements.
Definition: ptp_filters.h:187
void reset() override
Flush previous inputs and reset to a neutral state.
Definition: ptp_filters.h:195
s64 update(s64 next, u32 elapsed_usec) override
Method called for each new input sample.
Definition: ptp_filters.h:198
Convert normalized frequency offset to ticks-per-clock.
Definition: ptp_filters.h:545
s64 invert(s64 rate) const
Inverse conversion (ticks-per-clock ==> normalized rate)
Definition: ptp_filters.cc:500
bool ok() const
Is the scale coefficient large enough to mitigate rounding error?
Definition: ptp_filters.h:553
s64 convert(s64 offset) const
Forward conversion (normalized rate ==> ticks-per-clock)
Definition: ptp_filters.cc:495
constexpr RateConversion(double ref_clk_hz, unsigned scale_ns)
Specify the nominal clock frequency and the scale (see above).
Definition: ptp_filters.h:549
A sliding-window circular buffer, retaining the last N samples.
Definition: ptp_filters.h:96
Helper functions for manipulating singly-linked lists.
Definition: list.h:52
void push_back(T *item)
Add a new item at the tail of the list.
Definition: list.h:273
Templated functions for manipulating singly-linked lists.
High-precision "Time" object for use with PTP / IEEE1588.
Loop-filter coefficients for use with the "ControllerLR" class.
Definition: ptp_filters.h:400
constexpr CoeffLR(double tau_secs)
Calculate tracking-loop coefficients.
Definition: ptp_filters.h:403
bool ok() const
Are all coefficients large enough to mitigate rounding error?
Definition: ptp_filters.h:411
Loop-filter coefficients for use with the "ControllerPI" class.
Definition: ptp_filters.h:224
constexpr CoeffPI(double tau_secs)
Calculate tracking-loop coefficients.
Definition: ptp_filters.h:227
bool ok() const
Are all coefficients large enough to mitigate rounding error?
Definition: ptp_filters.h:235
static constexpr unsigned SCALE
Fixed-point scaling of each coefficient by 2^-N.
Definition: ptp_filters.h:239
Loop-filter coefficients for use with the "ControllerPII" class.
Definition: ptp_filters.h:295
static constexpr unsigned SCALE2
Fixed-point scaling of each coefficient by 2^-N.
Definition: ptp_filters.h:313
static constexpr unsigned SCALE
Fixed-point scaling of each coefficient by 2^-N.
Definition: ptp_filters.h:314
bool ok() const
Are all coefficients large enough to mitigate rounding error?
Definition: ptp_filters.h:307
static constexpr unsigned SCALE1
Fixed-point scaling of each coefficient by 2^-N.
Definition: ptp_filters.h:312
constexpr CoeffPII(double tau_secs)
Calculate tracking-loop coefficients.
Definition: ptp_filters.h:298
Stateless linear regression calculator.
Definition: ptp_filters.h:376
s64 extrapolate(s64 x) const
Extrapolate relative to the most recent sample.
Definition: ptp_filters.cc:373
satcat5::util::int128_t beta
Parameters for the best-fit line.
Definition: ptp_filters.h:381
constexpr LinearRegression()
Placeholder constructor.
Definition: ptp_filters.h:385
satcat5::util::int128_t alpha
Parameters for the best-fit line.
Definition: ptp_filters.h:380
static constexpr unsigned TSCALE
Parameters for the best-fit line.
Definition: ptp_filters.h:379
constexpr double pow2d(unsigned n)
Calculate 2^N for very large N, returning a double.
Definition: utils.h:255
Wide-integer arithmetic.