EmrysFilter 0.1.0
State estimation utilities in C++23 using Eigen
Loading...
Searching...
No Matches
RbUpdate.h
Go to the documentation of this file.
1#pragma once
2
9
10#include <cmath>
11#include <cstddef>
12#include <expected>
13#include <optional>
14#include <span>
15#include <type_traits>
16#include <utility>
17#include <vector>
18
37
38namespace Emrys
39{
40
41namespace internal
42{
43
45template <typename Input>
46[[nodiscard]] decltype(auto)
47meanOf(Input const & input)
48{
49 if constexpr (UncertainInput<Input>)
50 {
51 return input.mean();
52 }
53 else
54 {
55 return (input);
56 }
57}
58
63template <typename Scalar, typename Allocator = DefaultAllocator>
64struct PointLikelihoods
65{
67 explicit PointLikelihoods(Allocator const & allocator = {})
68 : logLikelihoodData(ReBound<Allocator, Scalar>(allocator))
69 , nisData(ReBound<Allocator, Scalar>(allocator))
70 , errorData(ReBound<Allocator, std::optional<GaussianError>>(allocator))
71 , batchData(ReBound<Allocator, Scalar>(allocator))
72 {}
73
75 void
76 reject(Eigen::Index count, Eigen::Index size)
77 {
78 logLikelihoodData.assign(std::size_t(count), -Eigen::NumTraits<Scalar>::infinity());
79 nisData.assign(std::size_t(count), Eigen::NumTraits<Scalar>::quiet_NaN());
80 errorData.assign(std::size_t(count), std::nullopt);
81 innovationSize = size;
82 }
83
85 void
86 reserve(Eigen::Index count)
87 {
88 logLikelihoodData.reserve(std::size_t(count));
89 nisData.reserve(std::size_t(count));
90 errorData.reserve(std::size_t(count));
91 }
92
94 [[nodiscard]] std::expected<void, GaussianError>
95 firstError() const
96 {
97 for (auto const & error : errorData)
98 {
99 if (error)
100 {
101 return std::unexpected{*error};
102 }
103 }
104 return {};
105 }
106
108 [[nodiscard]] Eigen::Index
109 size() const
110 {
111 return Eigen::Index(logLikelihoodData.size());
112 }
113
115 [[nodiscard]] auto
116 logLikelihoods()
117 {
118 return Eigen::Map<Eigen::Array<Scalar, Eigen::Dynamic, 1>>{logLikelihoodData.data(), size()};
119 }
120
122 [[nodiscard]] auto
123 logLikelihoods() const
124 {
125 return Eigen::Map<Eigen::Array<Scalar, Eigen::Dynamic, 1> const>{logLikelihoodData.data(), size()};
126 }
127
129 [[nodiscard]] auto
130 nis()
131 {
132 return Eigen::Map<Eigen::Array<Scalar, Eigen::Dynamic, 1>>{nisData.data(), size()};
133 }
134
136 [[nodiscard]] auto
137 nis() const
138 {
139 return Eigen::Map<Eigen::Array<Scalar, Eigen::Dynamic, 1> const>{nisData.data(), size()};
140 }
141
142 Buffer<Scalar, Allocator> logLikelihoodData;
143 Buffer<Scalar, Allocator> nisData;
144 Buffer<std::optional<GaussianError>, Allocator> errorData;
145 Buffer<Scalar, Allocator> batchData;
146 Eigen::Index innovationSize = 0;
147};
148
152struct PointLikelihoodsAccess
153{
155 template <typename Result>
156 [[nodiscard]] static auto &
157 points(Result & result)
158 {
159 return result.m_points;
160 }
161
163 template <typename Result>
164 [[nodiscard]] static auto &
165 cells(Result & result)
166 {
167 return result.m_cells;
168 }
169};
170
190template <ExecutionPolicy P, typename Points, typename Store, Measurement M, typename Likelihoods>
191[[nodiscard]] std::expected<void, GaussianError>
192updateGivenShared(
193 P && policy,
194 Points const & points,
195 Store const & conditionals,
196 Eigen::Index reference,
197 M const & measurement,
198 Likelihoods & result,
199 Store & posterior
200)
201{
202 using Scalar = typename Store::Scalar;
203 static_assert(Store::Shared);
204 auto const f =
205 PinnedMeasurement<typename Points::SpaceType, typename Points::PathType, M>{points.space(), measurement};
206 auto const & resolved = f.resolved();
207 using R = std::remove_cvref_t<decltype(resolved)>;
208 static_assert(
209 R::HasMethod,
210 "a Kalman update pushes the prior through the model by a method: observe(method, model, z)"
211 );
212 static_assert(
213 R::HasAdditiveNoise && !R::MixtureNoise && R::InputCount == 0,
214 "a shared covariance needs additive Gaussian noise and no uncertain input; use PerPointCovariance for mixture "
215 "noise or noise through the function"
216 );
217 static_assert(!isIterated<typename R::Method>, "an update given a block does not support Iterated yet");
218 static_assert(
219 !hasConsider<R>,
220 "an update given a block, as a Rao-Blackwellized filter's, does not consider blocks yet (decisions.md "
221 "[consider-states])"
222 );
223 EMRYS_FILTER_ASSERT(
224 conditionals.size() == points.size() && conditionals.size() > 0
225 && "give one Gaussian per point, and at least one point"
226 );
227 auto const prior = conditionals.conditional(reference);
228 auto const predicted = withPoint(points, reference, [&](auto const & pinned) {
229 return transformPrediction(resolved, resolved.method(), f, withCross(prior), pinned);
230 });
231 auto const & y = predicted.output();
232 auto const & cross = predicted.crossCovariance();
233 auto const & noise = resolved.noise().distribution();
234 using InnovationMatrix = typename std::remove_cvref_t<decltype(y)>::MatrixType;
235 using InnovationVector = typename std::remove_cvref_t<decltype(y)>::DifferenceVectorType;
236 InnovationMatrix s = y.covarianceLower() + noise.covarianceLower();
237 auto const factored = innovationCholesky<typename R::InnovationRepair>(s);
238 if (!factored)
239 {
240 return std::unexpected{factored.error()};
241 }
242 auto const & llt = *factored;
243 // With nothing left Gaussian, only the likelihoods remain; MSVC can't form an empty gain.
244 constexpr bool NothingLeft = std::remove_cvref_t<decltype(cross)>::RowsAtCompileTime == 0;
245 auto const gain = [&] {
246 if constexpr (NothingLeft)
247 {
248 return cross;
249 }
250 else
251 {
252 return transposedSolve<std::remove_cvref_t<decltype(predicted.crossCovariance())>>(
253 llt,
254 predicted.crossCovariance().transpose()
255 );
256 }
257 }();
258 Eigen::Index const m = y.differenceDimension();
259 Scalar const logNormalizer =
260 Scalar(-0.5) * (Scalar(m) * log2Pi<Scalar>() + Scalar(2) * llt.matrixLLT().diagonal().array().log().sum());
261
262 result.reject(points.size(), m);
263 // Each posterior mean is written once, from the prior's, rather than copied and then moved.
264 posterior.resizeLike(conditionals);
265 auto posteriorMeans = posterior.means();
266 forEachIndex(std::forward<P>(policy), points.size(), [&](Eigen::Index i) {
267 // The point's own predicted measurement, moved by the noise's mean, and the observation's
268 // residual from it.
269 InnovationVector residual = observedResidual<Scalar>(
270 resolved,
271 y.space(),
272 Emrys::plus(y.space(), valueOf(f(conditionals.mean(i), points.center(i))), noise.mean())
273 );
274 Scalar const nis = residual.dot(llt.solve(residual));
275 result.nisData[std::size_t(i)] = nis;
276 result.logLikelihoodData[std::size_t(i)] = logNormalizer - Scalar(0.5) * nis;
277 if constexpr (!NothingLeft)
278 {
279 posteriorMeans.col(i) = Emrys::plus(conditionals.space(), conditionals.mean(i), multiply(gain, residual));
280 }
281 });
282 if constexpr (!NothingLeft)
283 {
284 // The covariance update in the measurement's form; updates::Joseph's R is the noise plus the
285 // linearization error of the measurement before it, N + Omega from the prediction's parts.
286 using Form = std::remove_cvref_t<decltype(resolved.covarianceUpdate())>;
287 using CovMatrix = std::remove_cvref_t<decltype(posterior.sharedCovariance())>;
288 CovMatrix symmetric = prior.covarianceLower();
289 symmetric.template triangularView<Eigen::StrictlyUpper>() = symmetric.transpose();
290 posterior.sharedCovariance() = posteriorCov(resolved.covarianceUpdate(), symmetric, gain, cross, s, [&] {
291 if constexpr (std::is_same_v<Form, updates::Joseph>)
292 {
293 auto linearization = statisticalLinearization(predicted);
294 using Linearization = decltype(linearization);
295 return JosephTerms<typename Linearization::JacobianMatrix, InnovationMatrix>{
296 std::move(linearization.jacobian),
297 InnovationMatrix{linearization.errorCov + noise.covarianceLower()}
298 };
299 }
300 else
301 {
302 return std::tuple<>{};
303 }
304 });
305 }
306 return {};
307}
308
323template <ExecutionPolicy P, typename Points, typename Store, Measurement M, typename Policy, typename Likelihoods>
324[[nodiscard]] std::expected<void, GaussianError>
325updateGivenEach(
326 P && executionPolicy,
327 Points const & points,
328 Store const & conditionals,
329 M const & measurement,
330 Policy const & policy,
331 Likelihoods & result,
332 Store & posterior
333)
334{
335 using ConditionalType = typename Store::ConditionalType;
336 static_assert(!Store::Shared);
337 EMRYS_FILTER_ASSERT(
338 conditionals.size() == points.size() && conditionals.size() > 0
339 && "give one Gaussian per point, and at least one point"
340 );
341 // Each posterior is written once: the policy's, or the prior's where it rejected the point.
342 posterior.resizeLike(conditionals);
343 // Every point starts rejected; the first point's update gives the innovation's size.
344 result.reject(points.size(), 0);
345 // The measurement over the rest and a point, resolved once for every point.
346 auto const f =
347 PinnedMeasurement<typename Points::SpaceType, typename Points::PathType, M>{points.space(), measurement};
348 forEachIndex(std::forward<P>(executionPolicy), points.size(), [&](Eigen::Index i) {
349 // MSVC loses i in a nested generic lambda that captures it implicitly, so it's named.
350 auto updated = withPoint(points, i, [&, i](auto const & pinned) {
351 return updateBranchesWith<true>(conditionals.conditional(i), f, pinned);
352 });
353 if (!updated)
354 {
355 result.errorData[std::size_t(i)] = updated.error();
356 return;
357 }
358 auto & measured = *updated;
359 if (i == 0)
360 {
361 result.innovationSize = measured.innovationSize();
362 }
363 static_assert(
364 ReductionPolicy<Policy, std::remove_cvref_t<decltype(measured)>, ConditionalType>,
365 "a Rao-Blackwellized filter keeps one Gaussian per point, so its policy must keep one: "
366 "Collapse, Select, or a Gate over them"
367 );
368 // The policy writes what it keeps to the point's posterior.
369 using Scalar = typename std::remove_cvref_t<decltype(measured)>::Scalar;
370 auto out = OneComponentOutput<ConditionalType, Scalar>{posterior.conditional(i)};
371 reduceInto(policy, std::move(measured.branches), out);
372 if (!out.added)
373 {
374 posterior.conditional(i) = conditionals.conditional(i);
375 return;
376 }
377 std::size_t best = 0;
378 for (std::size_t k = 1; k < measured.branches.size(); ++k)
379 {
380 if (measured.branches[k].logWeight > measured.branches[best].logWeight)
381 {
382 best = k;
383 }
384 }
385 result.nisData[std::size_t(i)] = measured.branches[best].nis();
386 // The point is weighted by the likelihood of the branches what it kept stands for.
387 result.logLikelihoodData[std::size_t(i)] = measured.logLikelihood() + out.logWeight;
388 });
389 return result.firstError();
390}
391
395template <typename Pinned, typename Read>
396consteval void
397assertReadsPinned()
398{
399 static_assert(
400 pathLiesIn<Read, Pinned>,
401 "logLikelihood() may read only the gridded or sampled block: with the rest conditioned on it, "
402 "the rest would no longer be Gaussian"
403 );
404}
405
421template <ExecutionPolicy P, typename Points, typename Store, WeighsPoints M, typename Likelihoods>
422void
423updateGivenLikelihood(
424 P && policy,
425 Points const & points,
426 Store const & conditionals,
427 M const & measurement,
428 Likelihoods & result
429)
430{
431 using Scalar = typename Store::Scalar;
432 using Pinned = typename Points::PathType;
433 auto const resolved = resolveMeasurement(measurement, points.space());
434 using R = std::remove_cvref_t<decltype(resolved)>;
435 static_assert(R::HasLogLikelihood, "a measurement weighted by its likelihood alone has logLikelihood(g)");
436 static_assert(
437 !hasConsider<R>,
438 "consider() is a Kalman update's; a filter that weights points moves no block it does not condition "
439 "(decisions.md [consider-states])"
440 );
441 [&]<typename... Paths>(TagPaths<Paths...> *) {
442 (assertReadsPinned<Pinned, Paths>(), ...);
443 }(static_cast<typename R::StatePaths *>(nullptr));
444 EMRYS_FILTER_ASSERT(
445 conditionals.size() == points.size() && conditionals.size() > 0
446 && "give one Gaussian per point, and at least one point"
447 );
448 Eigen::Index const start = points.pinnedRows().start;
449 result.reject(points.size(), 0);
450 if constexpr (R::BatchedLikelihood)
451 {
452 fillBatchedLogLikelihoods(
453 std::forward<P>(policy),
454 resolved,
455 points.size(),
456 [&](Eigen::Index i) {
457 return points.center(i);
458 },
459 start,
460 result.logLikelihoodData.data(),
461 result.batchData
462 );
463 }
464 else
465 {
466 forEachIndex(std::forward<P>(policy), points.size(), [&](Eigen::Index i) {
467 result.logLikelihoodData[std::size_t(i)] = Scalar(resolved.logLikelihoodAtRows(points.center(i), start));
468 });
469 }
470}
471
483template <typename Points, typename Store, typename Likelihoods, typename OneResult, typename UpdateOne>
484[[nodiscard]] std::expected<void, NumericError>
485updateHybridsGiven(
486 Points const & points,
487 Store & hybrids,
488 Likelihoods & likelihoods,
489 OneResult & oneResult,
490 UpdateOne const & updateOne
491)
492{
493 likelihoods.reject(points.size(), 0);
494 for (Eigen::Index i = 0; i < points.size(); ++i)
495 {
496 auto & hybrid = hybrids.conditional(i);
497 auto const updated = withPoint(points, i, [&](auto const & pinned) {
498 return updateOne(hybrid, pinned, oneResult);
499 });
500 if (!updated)
501 {
502 return std::unexpected{updated.error()};
503 }
504 if (i == 0)
505 {
506 likelihoods.innovationSize = oneResult.modes.front().innovationSize();
507 }
508 if (!oneResult.accepted)
509 {
510 continue;
511 }
512 likelihoods.logLikelihoodData[std::size_t(i)] = oneResult.logLikelihood();
513 // The NIS of the most probable mode after the update, and of its most probable branch.
514 Eigen::Index mode = 0;
515 hybrid.modes().logWeights().maxCoeff(&mode);
516 auto const & branches = oneResult.modes[std::size_t(mode)].branches;
517 std::size_t best = 0;
518 for (std::size_t k = 1; k < branches.size(); ++k)
519 {
520 if (branches[k].logWeight > branches[best].logWeight)
521 {
522 best = k;
523 }
524 }
525 likelihoods.nisData[std::size_t(i)] = branches[best].nis();
526 }
527 return {};
528}
529
533template <typename Model>
534inline constexpr bool isSwitchingPointModel = requires { typename Model::ModeSpaceType; };
535
539template <typename Model>
540using PointCovarianceFor = std::conditional_t<isSwitchingPointModel<Model>, PerPointCovariance, SharedCovariance>;
541
543template <typename Model_, typename Method_>
544class PointSteps
545{
546public:
547
549 PointSteps() = default;
550
552 template <typename Allocator>
553 PointSteps(Model_ model, MethodFor<Model_, Method_> method, Allocator const &)
554 : m_model{std::move(model)}
555 , m_method{predictionMethod(m_model, std::move(method))}
556 {}
557
559 [[nodiscard]] Model_ const &
560 model() const
561 {
562 return m_model;
563 }
564
566 [[nodiscard]] Model_ &
567 model()
568 {
569 return m_model;
570 }
571
573 [[nodiscard]] Method_ const &
574 method() const
575 {
576 return m_method;
577 }
578
579private:
580
581 Model_ m_model;
582 EMRYS_FILTER_NO_UNIQUE_ADDRESS Method_ m_method;
583};
584
585} // namespace internal
586
587} // namespace Emrys
The allocator conventions of decisions.md [allocation]: DefaultAllocator, Capacity,...
What the Rao-Blackwellized states share (decisions.md [rao-blackwellized-core], [rb-particle-filter])...
Execution policies (decisions.md [execution-policies]): execution::sequential, the ExecutionPolicy co...
The Gaussian: its parameters GaussianParams, the Gaussian that evaluates the density,...
The likelihoods that sample-based filters weight by (decisions.md [likelihood-measurements],...
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]...
Moments: the mean and covariance that a Gaussian carries and that a mixture or a sample set is moment...
Products of fixed-size matrices by Eigen's fixed-size product at every size: multiply() is by Eigen'...
Reducing an update's branches (decisions.md [weighted-branches], [reduction-policies],...
SampleSet: a matrix whose columns are samples in a space, which the samplers return and the unscented...
Drawing samples: SamplesWithLogDensity and SampleWithLogDensity, the Gaussian draws drawSamples(),...
Solving by a factorization of a small matrix: transposedSolve() gives , the shape of a Kalman gain an...
The Space concept and what every continuous space shares (decisions.md [sizes-and-dimensions]): the t...
Weighted statistics over points, all in internal, which the containers of weighted elements (Gaussian...
Propagating a distribution through a function (decisions.md [method-per-measurement],...
Log-weights: the WeightError a normalization can fail with (NumericError.h), and the streaming log-su...
#define EMRYS_FILTER_NO_UNIQUE_ADDRESS
Assertion used for programmer errors (misuse of the API).
Definition Macros.h:38
Scalar log2Pi()
Definition Math.h:177
NumericError GaussianError
Reasons a Gaussian, in any of its forms (decisions.md [gaussian-forms]), can fail to give a density,...
Definition NumericError.h:45
constexpr auto symmetric(Fallbacks... fallbacks)
Definition Roots.h:1204
CrossMarked< Input > withCross(Input const &input)
Marks the uncertain input, usually the state, whose cross-covariance with the output transform() also...
Definition Transform.h:1649