EmrysFilter 0.1.0
State estimation utilities in C++23 using Eigen
Loading...
Searching...
No Matches
Consistency.h
Go to the documentation of this file.
1#pragma once
2
9
10#include <algorithm>
11#include <array>
12#include <cstddef>
13#include <expected>
14#include <limits>
15#include <type_traits>
16#include <utility>
17
26
27namespace Emrys
28{
29
36inline constexpr int Cumulative = -2;
37
38static_assert(Cumulative != Eigen::Dynamic, "a cumulative window is not a dynamic one");
39
61
62namespace internal
63{
64
72template <typename Derived, typename Scalar>
73class ChiSquaredTest
74{
75public:
76
80 [[nodiscard]] std::expected<Scalar, NumericError>
81 average() const
82 {
83 if (derived().count() == 0)
84 {
85 return std::unexpected{NumericError::Empty};
86 }
87 Scalar const total = derived().sum();
88 if (isNaN(total))
89 {
90 return std::unexpected{NumericError::NonFinite};
91 }
92 return total / internal::scalarFromCount<Scalar>(derived().degreesOfFreedom());
93 }
94
99 [[nodiscard]] std::expected<std::pair<Scalar, Scalar>, NumericError>
100 bounds(Scalar probability) const
101 {
102 if (derived().count() == 0)
103 {
104 return std::unexpected{NumericError::Empty};
105 }
106 Scalar const dof = internal::scalarFromCount<Scalar>(derived().degreesOfFreedom());
107 return std::pair<Scalar, Scalar>{
108 chiSquaredQuantile<Scalar>((Scalar(1) - probability) / Scalar(2), dof),
109 chiSquaredQuantile<Scalar>((Scalar(1) + probability) / Scalar(2), dof)
110 };
111 }
112
118 [[nodiscard]] std::expected<Consistency, NumericError>
119 verdict(Scalar probability) const
120 {
121 auto const range = bounds(probability);
122 if (!range)
123 {
124 return std::unexpected{range.error()};
125 }
126 Scalar const total = derived().sum();
127 if (isNaN(total))
128 {
129 return std::unexpected{NumericError::NonFinite};
130 }
131 if (total > range->second)
132 {
134 }
135 if (total < range->first)
136 {
138 }
140 }
141
142private:
143
145 [[nodiscard]] Derived const &
146 derived() const
147 {
148 return static_cast<Derived const &>(*this);
149 }
150};
151
157template <int Window, int MaxWindow, typename Scalar, typename Allocator>
158class ChiSquaredWindow : public ChiSquaredTest<ChiSquaredWindow<Window, MaxWindow, Scalar, Allocator>, Scalar>
159{
160 static_assert(Window == Eigen::Dynamic || Window > 0, "a window holds at least one statistic");
161 static_assert(
162 Window == Eigen::Dynamic ? MaxWindow == Eigen::Dynamic || MaxWindow > 0 : MaxWindow == Window,
163 "a window's bound is positive, and a fixed window is its own bound"
164 );
165
166 using Entry = std::pair<Scalar, Eigen::Index>;
167 static constexpr bool Inline = MaxWindow != Eigen::Dynamic;
168 using Entries = std::conditional_t<
169 Inline,
170 std::array<Entry, std::size_t(Inline ? MaxWindow : 1)>,
171 internal::Buffer<Entry, Allocator>>;
172 using Length = std::conditional_t<
173 Window == Eigen::Dynamic,
174 Eigen::Index,
175 std::integral_constant<Eigen::Index, Window == Eigen::Dynamic ? 1 : Window>>;
176
177public:
178
179 using allocator_type = Allocator;
180
182 ChiSquaredWindow()
183 requires(Window != Eigen::Dynamic)
184 = default;
185
189 ChiSquaredWindow(Eigen::Index window, Allocator const & allocator)
190 requires(Window == Eigen::Dynamic)
191 : m_window{window}
192 , m_entries(entries(window, allocator))
193 {
194 EMRYS_FILTER_ASSERT(window > 0 && "the window must hold at least one statistic");
195 EMRYS_FILTER_ASSERT((!Inline || window <= MaxWindow) && "the window must be within its bound");
196 }
197
199 ChiSquaredWindow(ChiSquaredWindow const & other, Allocator const & allocator)
200 requires(!Inline)
201 : m_window{other.m_window}
202 , m_entries(other.m_entries, internal::ReBound<Allocator, Entry>(allocator))
203 , m_count{other.m_count}
204 , m_next{other.m_next}
205 {}
206
208 [[nodiscard]] Allocator
209 get_allocator() const
210 requires(!Inline)
211 {
212 return Allocator(m_entries.get_allocator());
213 }
214
218 void
219 add(Scalar value, Eigen::Index dimension)
220 {
221 EMRYS_FILTER_ASSERT(dimension > 0 && "the statistic must have at least one degree of freedom");
222 m_entries[std::size_t(m_next)] = Entry{value, dimension};
223 m_next = m_next + 1 == window() ? 0 : m_next + 1;
224 m_count = std::min(m_count + 1, window());
225 }
226
228 void
229 reset()
230 {
231 m_count = 0;
232 m_next = 0;
233 }
234
236 [[nodiscard]] Eigen::Index
237 window() const
238 {
239 return m_window;
240 }
241
243 [[nodiscard]] Eigen::Index
244 count() const
245 {
246 return m_count;
247 }
248
250 [[nodiscard]] Scalar
251 sum() const
252 {
253 Scalar total = 0;
254 for (Eigen::Index i = 0; i < m_count; ++i)
255 {
256 total += m_entries[std::size_t(i)].first;
257 }
258 return total;
259 }
260
262 [[nodiscard]] Eigen::Index
263 degreesOfFreedom() const
264 {
265 Eigen::Index total = 0;
266 for (Eigen::Index i = 0; i < m_count; ++i)
267 {
268 total += m_entries[std::size_t(i)].second;
269 }
270 return total;
271 }
272
273private:
274
276 [[nodiscard]] static Entries
277 entries(Eigen::Index window, Allocator const & allocator)
278 {
279 if constexpr (Inline)
280 {
281 return Entries{};
282 }
283 else
284 {
285 return Entries(std::size_t(window), Entry{}, internal::ReBound<Allocator, Entry>(allocator));
286 }
287 }
288
289 EMRYS_FILTER_NO_UNIQUE_ADDRESS Length m_window{};
290 Entries m_entries{};
291 Eigen::Index m_count = 0;
292 Eigen::Index m_next = 0;
293};
294
298template <typename Scalar>
299class ChiSquaredSum : public ChiSquaredTest<ChiSquaredSum<Scalar>, Scalar>
300{
301public:
302
304 void
305 add(Scalar value, Eigen::Index dimension)
306 {
307 EMRYS_FILTER_ASSERT(dimension > 0 && "the statistic must have at least one degree of freedom");
308 m_sum += value;
309 m_degreesOfFreedom += dimension;
310 ++m_count;
311 }
312
314 void
315 reset()
316 {
317 m_sum = 0;
318 m_degreesOfFreedom = 0;
319 m_count = 0;
320 }
321
323 [[nodiscard]] Eigen::Index
324 count() const
325 {
326 return m_count;
327 }
328
330 [[nodiscard]] Scalar
331 sum() const
332 {
333 return m_sum;
334 }
335
337 [[nodiscard]] Eigen::Index
338 degreesOfFreedom() const
339 {
340 return m_degreesOfFreedom;
341 }
342
343private:
344
345 Scalar m_sum = 0;
346 Eigen::Index m_degreesOfFreedom = 0;
347 Eigen::Index m_count = 0;
348};
349
353template <int Window, int MaxWindow, typename Scalar, typename Allocator>
354using ChiSquaredWindowFor = std::conditional_t<
355 Window == Cumulative, // a sum since the first statistic, or a ring of the latest
356 ChiSquaredSum<Scalar>,
357 ChiSquaredWindow<Window, MaxWindow, Scalar, Allocator>>;
358
359} // namespace internal
360
415template <
416 int Window = Eigen::Dynamic,
417 int MaxWindow = Window,
418 typename Scalar = double,
419 typename Allocator = DefaultAllocator>
420class NisMonitor : public internal::ChiSquaredWindowFor<Window, MaxWindow, Scalar, Allocator>
421{
422 static_assert(Window != Cumulative || MaxWindow == Cumulative, "a cumulative window has no bound");
423
424 using Base = internal::ChiSquaredWindowFor<Window, MaxWindow, Scalar, Allocator>;
425
426public:
427
430 requires(Window != Eigen::Dynamic)
431 = default;
432
436 explicit NisMonitor(Eigen::Index window, Allocator const & allocator = {})
437 requires(Window == Eigen::Dynamic)
438 : Base{window, allocator}
439 {}
440
442 NisMonitor(NisMonitor const & other, Allocator const & allocator)
443 requires(Window == Eigen::Dynamic && MaxWindow == Eigen::Dynamic)
444 : Base{other, allocator}
445 {}
446
447 using Base::add;
448
452 template <typename Update>
453 requires requires(Update const & update) {
454 update.branches.front().nis();
455 update.innovationSize();
456 }
457 void
458 add(Update const & update)
459 {
460 EMRYS_FILTER_ASSERT(
461 update.branches.size() == 1 && "with several branches, pass the NIS to test with add(nis, dimension)"
462 );
463 add(update.branches.front().nis(), update.innovationSize());
464 }
465};
466
485template <GaussianForm Estimate, typename Derived>
486[[nodiscard]] std::expected<typename Estimate::Scalar, GaussianError>
487nees(Eigen::MatrixBase<Derived> const & truth, Estimate const & estimate)
488{
489 EMRYS_FILTER_ASSERT(truth.size() == estimate.valueDimension() && "the true state must fit the estimate's space");
490 if constexpr (requires { estimate.toDensity(); })
491 {
492 auto const density = estimate.toDensity();
493 if (!density)
494 {
495 return std::unexpected{density.error()};
496 }
497 return typename Estimate::Scalar(density->squaredMahalanobisDistance(truth));
498 }
499 else
500 {
501 auto const params = toGaussian(estimate);
502 if (!params)
503 {
504 return std::unexpected{params.error()};
505 }
506 return nees(truth, *params);
507 }
508}
509
525template <typename Path, GaussianForm Estimate, typename Derived>
526 requires requires(Estimate const & estimate) { estimate.template marginal<Path>(); }
527[[nodiscard]] std::expected<typename Estimate::Scalar, GaussianError>
528nees(Eigen::MatrixBase<Derived> const & truth, Estimate const & estimate)
529{
530 EMRYS_FILTER_ASSERT(truth.size() == estimate.valueDimension() && "the true state must fit the estimate's space");
531 // block() is a vector even for one coordinate, where truth(idx<Path>()) would be a scalar.
532 auto const block = Emrys::block<Path>(estimate.space(), truth);
533 auto const marginal = estimate.template marginal<Path>();
534 if constexpr (internal::isExpected<std::remove_cvref_t<decltype(marginal)>>)
535 {
536 if (!marginal)
537 {
538 return std::unexpected{marginal.error()};
539 }
540 return nees(block, *marginal);
541 }
542 else
543 {
544 return nees(block, marginal);
545 }
546}
547
592template <
593 int Window = Eigen::Dynamic,
594 int MaxWindow = Window,
595 typename Scalar = double,
596 typename Allocator = DefaultAllocator>
597class NeesMonitor : public internal::ChiSquaredWindowFor<Window, MaxWindow, Scalar, Allocator>
598{
599 static_assert(Window != Cumulative || MaxWindow == Cumulative, "a cumulative window has no bound");
600
601 using Base = internal::ChiSquaredWindowFor<Window, MaxWindow, Scalar, Allocator>;
602
603public:
604
607 requires(Window != Eigen::Dynamic)
608 = default;
609
613 explicit NeesMonitor(Eigen::Index window, Allocator const & allocator = {})
614 requires(Window == Eigen::Dynamic)
615 : Base{window, allocator}
616 {}
617
619 NeesMonitor(NeesMonitor const & other, Allocator const & allocator)
620 requires(Window == Eigen::Dynamic && MaxWindow == Eigen::Dynamic)
621 : Base{other, allocator}
622 {}
623
624 using Base::add;
625
631 template <GaussianForm Estimate, typename Derived>
632 requires requires(Estimate const & estimate, Eigen::MatrixBase<Derived> const & truth) {
633 nees(truth, estimate);
634 }
635 void
636 add(Eigen::MatrixBase<Derived> const & truth, Estimate const & estimate)
637 {
638 auto const value = nees(truth, estimate);
639 add(value ? Scalar(*value) : std::numeric_limits<Scalar>::quiet_NaN(), estimate.differenceDimension());
640 }
641
647 template <typename Path, typename Derived, typename Estimate>
648 requires requires(Estimate const & estimate, Eigen::MatrixBase<Derived> const & truth) {
649 nees<Path>(truth, estimate);
650 }
651 void
652 add(Eigen::MatrixBase<Derived> const & truth, Estimate const & estimate)
653 {
654 auto const value = nees<Path>(truth, estimate);
655 add(value ? Scalar(*value) : std::numeric_limits<Scalar>::quiet_NaN(),
656 internal::differenceBlockRows<Path>(estimate.space()).size);
657 }
658};
659
660} // namespace Emrys
The allocator conventions of decisions.md [allocation]: DefaultAllocator, Capacity,...
The chi-squared distribution's CDF and quantile, with no dependency outside Eigen,...
What the four forms of the Gaussian share (decisions.md [gaussian-forms], [form-densities]): the Gaus...
The macros every header includes first.
Mathematical constants and scalar functions that work in every scalar type the library accepts,...
The update of a Gaussian by a measurement (decisions.md [templated-measurements], [weighted-branches]...
NumericError, the reasons a factorization or a normalization can fail, and its two names,...
Tag paths and the index utilities (decisions.md [tag-addressing], [index-names]): TagPath and TagPath...
NeesMonitor()=default
A monitor over the most recent Window estimates, or over every estimate for Cumulative.
NeesMonitor(NeesMonitor const &other, Allocator const &allocator)
Copies other into a window from allocator.
Definition Consistency.h:619
NeesMonitor(Eigen::Index window, Allocator const &allocator={})
A monitor over the most recent window estimates, whose window, if it has no bound,...
Definition Consistency.h:613
void add(Eigen::MatrixBase< Derived > const &truth, Estimate const &estimate)
Adds the estimate estimate of the true state truth: its nees(), with its difference dimension as the ...
Definition Consistency.h:636
void add(Eigen::MatrixBase< Derived > const &truth, Estimate const &estimate)
Adds the block of estimate named by the path Path at the same block of the true state truth: its nees...
Definition Consistency.h:652
void add(Update const &update)
Adds an update with a single branch, as from Gaussian noise: a MeasurementUpdate, or the FilterUpdate...
Definition Consistency.h:458
NisMonitor()=default
A monitor over the most recent Window updates, or over every update for Cumulative.
NisMonitor(Eigen::Index window, Allocator const &allocator={})
A monitor over the most recent window updates, whose window, if it has no bound, comes from allocator...
Definition Consistency.h:436
NisMonitor(NisMonitor const &other, Allocator const &allocator)
Copies other into a window from allocator.
Definition Consistency.h:442
std::allocator< std::byte > DefaultAllocator
The allocator a type with a run-time count uses unless given one.
Definition Allocation.h:47
Scalar chiSquaredQuantile(std::type_identity_t< Scalar > const &probability, std::type_identity_t< Scalar > const &dof)
Definition ChiSquared.h:148
#define EMRYS_FILTER_NO_UNIQUE_ADDRESS
Assertion used for programmer errors (misuse of the API).
Definition Macros.h:38
bool isNaN(T const &x)
Definition Math.h:329
NumericError
Reasons a computation from parameters the caller gave can fail: a matrix that does not factorize or a...
Definition NumericError.h:30
@ NonFinite
a matrix, a factor, or a log-weight holds NaN or infinity (+inf, for a log-weight)
Definition NumericError.h:33
@ Empty
there are no weights
Definition NumericError.h:34
std::expected< GaussianParams< typename G::SpaceType, typename G::Scalar >, GaussianError > toGaussian(G const &form)
Definition GaussianForm.h:91
constexpr int Cumulative
The window of a NisMonitor or NeesMonitor that drops nothing: its test runs over every statistic adde...
Definition Consistency.h:36
Consistency
The verdict of a NisMonitor's or NeesMonitor's verdict(): where the sum of the window's statistics li...
Definition Consistency.h:56
std::expected< typename Estimate::Scalar, GaussianError > nees(Eigen::MatrixBase< Derived > const &truth, Estimate const &estimate)
Definition Consistency.h:487
@ Consistent
the sum lies within the bounds
Definition Consistency.h:57
@ Underconfident
the sum lies below the lower bound: the covariance overstates the errors
Definition Consistency.h:59
@ Overconfident
the sum lies above the upper bound: the covariance understates the errors
Definition Consistency.h:58
std::expected< CategoricalUpdate< Space, Scalar >, WeightError > update(CategoricalParams< Space, Scalar > const &prior, E const &emission)
Definition Emission.h:97
auto block(Space auto const &space, X &&x)
Definition TagPath.h:979