EmrysFilter 0.1.0
State estimation utilities in C++23 using Eigen
Loading...
Searching...
No Matches
IteratedUpdate.h
Go to the documentation of this file.
1#pragma once
2
9
10#include <array>
11#include <concepts>
12#include <cstddef>
13#include <expected>
14#include <optional>
15#include <span>
16#include <tuple>
17#include <type_traits>
18#include <utility>
19#include <variant>
20#include <vector>
21
22#include <Eigen/Cholesky>
23
42
43namespace Emrys
44{
45
46namespace internal
47{
48
53template <typename Input>
54class MovedInput
55{
56public:
57
58 using Scalar = typename Input::Scalar;
59 using SpaceType = typename Input::SpaceType;
60
62 static constexpr int ValueDimensionAtCompileTime = Input::ValueDimensionAtCompileTime;
63
65 static constexpr int MaxValueDimensionAtCompileTime = Input::MaxValueDimensionAtCompileTime;
66
68 static constexpr int DifferenceDimensionAtCompileTime = Input::DifferenceDimensionAtCompileTime;
69
71 static constexpr int MaxDifferenceDimensionAtCompileTime = Input::MaxDifferenceDimensionAtCompileTime;
72
74 using VectorType =
75 Eigen::Matrix<Scalar, ValueDimensionAtCompileTime, 1, Eigen::ColMajor, MaxValueDimensionAtCompileTime, 1>;
76
78 MovedInput(Input const & input, VectorType mean)
79 : m_input{input}
80 , m_mean{std::move(mean)}
81 {}
82
84 [[nodiscard]] decltype(auto)
85 space() const
86 {
87 return m_input.space();
88 }
89
91 [[nodiscard]] VectorType const &
92 mean() const
93 {
94 return m_mean;
95 }
96
98 [[nodiscard]] decltype(auto)
99 covarianceLower() const
100 {
101 return m_input.covarianceLower();
102 }
103
105 [[nodiscard]] Eigen::Index
106 valueDimension() const
107 {
108 return m_input.valueDimension();
109 }
110
112 [[nodiscard]] Eigen::Index
113 differenceDimension() const
114 {
115 return m_input.differenceDimension();
116 }
117
118private:
119
120 Input const & m_input;
121 VectorType m_mean;
122};
123
128template <typename Input, typename Cov>
129class RespreadInput : public MovedInput<Input>
130{
131public:
132
134 RespreadInput(Input const & input, typename MovedInput<Input>::VectorType mean, Cov const & cov)
135 : MovedInput<Input>{input, std::move(mean)}
136 , m_cov{cov}
137 {}
138
140 [[nodiscard]] Cov const &
141 covarianceLower() const
142 {
143 return m_cov;
144 }
145
146private:
147
148 Cov const & m_cov;
149};
150
157template <typename M, typename Component>
158class WithNoiseComponent
159{
160public:
161
162 using MethodType = typename M::MethodType;
163 using ModelType = typename M::ModelType;
164 using ArgumentsType = typename M::ArgumentsType;
165 using FormType = typename M::FormType;
166 using InnovationRepair = typename M::InnovationRepair;
167 using ConsiderType = typename M::ConsiderType;
168 using NoiseType = AdditiveNoise<Component const &>;
169
170 static constexpr bool HasMethod = M::HasMethod;
171 static constexpr bool HasObservation = M::HasObservation;
172 static constexpr bool OwnNoise = true;
173
175 WithNoiseComponent(M const & measurement, Component const & component)
176 : m_measurement{measurement}
177 , m_noise{component}
178 {}
179
181 [[nodiscard]] MethodType const &
182 method() const
183 {
184 return m_measurement.method();
185 }
186
188 [[nodiscard]] ModelType const &
189 model() const
190 {
191 return m_measurement.model();
192 }
193
195 [[nodiscard]] NoiseType const &
196 noise() const
197 {
198 return m_noise;
199 }
200
202 [[nodiscard]] decltype(auto)
203 observation() const
204 {
205 return m_measurement.observation();
206 }
207
209 [[nodiscard]] ArgumentsType const &
210 arguments() const
211 {
212 return m_measurement.arguments();
213 }
214
216 [[nodiscard]] decltype(auto)
217 covarianceUpdate() const
218 {
219 return m_measurement.covarianceUpdate();
220 }
221
222private:
223
224 M const & m_measurement;
225 NoiseType m_noise;
226};
227
229template <typename M, typename Component>
230inline constexpr bool isObserved<WithNoiseComponent<M, Component>>{true};
231
233
238template <typename Cov>
239[[nodiscard]] auto
240iterationScale(Eigen::MatrixBase<Cov> const & cov, typename Cov::Scalar tolerance)
241{
242 using Scalar = typename Cov::Scalar;
243 EMRYS_FILTER_ASSERT(tolerance >= Scalar(0) && "an iteration's tolerance must not be negative");
244 using Result = Eigen::Matrix<Scalar, Cov::RowsAtCompileTime, 1, Eigen::ColMajor, Cov::MaxRowsAtCompileTime, 1>;
245 Result result = cov.diagonal().cwiseSqrt() * tolerance;
246 return result;
247}
248
252template <typename Next, typename Delta, typename Scale>
253[[nodiscard]] bool
254withinScale(
255 Eigen::MatrixBase<Next> const & next,
256 Eigen::MatrixBase<Delta> const & delta,
257 Eigen::MatrixBase<Scale> const & scale
258)
259{
260 return ((next - delta).cwiseAbs().array() <= scale.array()).all();
261}
262
269template <typename Reads, typename Jacobian, typename ReadMean, typename ReadStep>
270void
271toPriorChart(
272 Reads const & reads,
273 Eigen::MatrixBase<Jacobian> const & jacobian,
274 ReadMean const & readMean,
275 ReadStep const & readStep
276)
277{
279 {
280 using Scalar = typename Jacobian::Scalar;
281 using Result = MatrixOf<
282 Scalar,
283 Jacobian::RowsAtCompileTime,
284 Reads::DifferenceSize,
285 Jacobian::MaxRowsAtCompileTime,
286 Reads::DifferenceMaxSize>;
287 Result result(jacobian.rows(), jacobian.cols());
288 result.noalias() = multiply(jacobian, resetJacobian(reads.space(), readMean, readStep));
289 jacobian.const_cast_derived() = result;
290 }
291}
292
309template <
310 typename Space,
311 typename Scalar,
312 typename R,
313 typename Method,
314 typename Reads,
315 typename Read,
316 typename F,
317 typename StateRead,
318 typename ReadMean,
319 typename ReadStep,
320 typename SpreadCov,
321 typename Use>
322[[nodiscard]] decltype(auto)
323linearizeReadsAt(
324 R const & resolved,
325 Method const & method,
326 Reads const & reads,
327 Read const & read,
328 F const & f,
329 StateRead const & stateRead,
330 ReadMean const & readMean,
331 bool moved,
332 ReadStep const & readStep,
333 SpreadCov const & spreadCov,
334 Use const & use
335)
336{
337 using ReadVector = typename MovedInput<Read>::VectorType;
338 if constexpr (!isLinearMethod<Method>)
339 {
340 using ReadMatrix = SpreadCov;
341 auto const input = RespreadInput<Read, ReadMatrix>{
342 read,
343 moved ? ReadVector{Emrys::plus(reads.space(), readMean, readStep)} : readMean,
344 spreadCov
345 };
346 // The root form's H_S = Psi^T S^-1, a solve with the root that spread the points, and N + Omega,
347 // its parts, so that spreadCov is neither factorized again nor subtracted from (decisions.md
348 // [linearization-forms]).
349 auto const predicted = transformMeasurement(resolved, method, f, input);
350 using InnovationGaussian = std::remove_cvref_t<decltype(predicted.output())>;
351 using InnovationMatrix = typename InnovationGaussian::MatrixType;
352 using Predicted = OutputWithCross<Space, typename InnovationGaussian::SpaceType, JacobianForm, Scalar>;
353 using ReadJacobianMatrix = MatrixOf<
354 Scalar,
355 InnovationGaussian::DifferenceDimensionAtCompileTime,
356 Reads::DifferenceSize,
357 InnovationGaussian::MaxDifferenceDimensionAtCompileTime,
358 Reads::DifferenceMaxSize>;
359 ReadJacobianMatrix jacobian = predicted.jacobian();
360 InnovationMatrix cov = predicted.inputsCovariance() + predicted.unexplainedCovariance();
361 InnovationMatrix const error = cov;
362 typename InnovationGaussian::VectorType mean = predicted.output().mean();
363 if (moved)
364 {
365 toPriorChart(reads, jacobian, readMean, readStep);
366 auto back = typename InnovationGaussian::DifferenceVectorType{};
367 back.noalias() = -multiply(jacobian, readStep);
368 mean = Emrys::plus(predicted.output().space(), predicted.output().mean(), back);
369 }
370 auto cross = typename Predicted::CrossMatrix{};
371 cross.noalias() = multiply(stateRead, jacobian.transpose());
372 cov.noalias() += multiply(jacobian, std::as_const(cross)(reads.differenceIndices(), Eigen::placeholders::all));
373 auto const y = InnovationGaussian{predicted.output().space(), std::move(mean), std::move(cov)};
374 return use(y, cross, ReadLinearization{jacobian, reads.differenceIndices(), [&] {
375 return error;
376 }});
377 }
378 else
379 {
380 auto const input =
381 MovedInput<Read>{read, moved ? ReadVector{Emrys::plus(reads.space(), readMean, readStep)} : readMean};
382 return linearizeMeasurement(
383 resolved,
384 method,
385 f,
386 input,
387 [&](auto const & problem, auto const & linearized) -> decltype(auto) {
388 using Problem = std::remove_cvref_t<decltype(problem)>;
389 using Predicted = OutputWithCross<Space, typename Problem::OutputSpaceType, JacobianForm, Scalar>;
390 Eigen::Index const readSize = reads.differenceIndices().size();
391 Eigen::Index const offset = problem.differenceOffset(0);
392 typename Problem::JacobianMatrix jacobian = linearized.jacobian;
393 typename Problem::template OutputVector<Scalar> mean = linearized.mean;
394 auto readJacobian = jacobian.template middleCols<Reads::DifferenceSize>(offset, readSize);
395 if (moved)
396 {
397 toPriorChart(reads, readJacobian, readMean, readStep);
398 auto back = typename Problem::template OutputDifferenceVector<Scalar>{};
399 back.noalias() = -multiply(readJacobian, readStep);
400 mean = Emrys::plus(problem.outputSpace(), linearized.mean, back);
401 }
402 auto cross = typename Predicted::CrossMatrix{};
403 cross.noalias() = multiply(stateRead, readJacobian.transpose());
404 auto const linearization =
405 ReadLinearization{readJacobian, reads.differenceIndices(), [&] {
406 using Error = typename Problem::OutputMatrix;
407 return inputsCovariance<Error>(resolved, problem, jacobian);
408 }};
409 if constexpr (R::InputCount > 0)
410 {
411 typename Problem::CrossMatrix const joint =
412 multiply(problem.covarianceLower(), jacobian.transpose());
413 auto const y = PredictionMoments{problem.outputSpace(), mean, jacobian, joint};
414 return use(y, cross, linearization);
415 }
416 else
417 {
418 auto const joint = std::as_const(cross)(reads.differenceIndices(), Eigen::placeholders::all);
419 auto const y = PredictionMoments{problem.outputSpace(), mean, jacobian, joint};
420 return use(y, cross, linearization);
421 }
422 }
423 );
424 }
425}
426
436template <
437 bool Statistics,
438 typename Prior,
439 typename R,
440 typename Reads,
441 typename Read,
442 typename F,
443 typename StateRead,
444 typename Sink>
445[[nodiscard]] decltype(auto)
446iteratedUpdate(
447 Prior const & prior,
448 R const & resolved,
449 Reads const & reads,
450 Read const & read,
451 F const & f,
452 StateRead const & stateRead,
453 Sink && sink
454)
455{
456 using Scalar = typename Prior::Scalar;
457 using Space = typename Prior::SpaceType;
458 using Difference = typename GaussianParams<Space, Scalar>::DifferenceVectorType;
459 using ReadVector = typename MovedInput<Read>::VectorType;
461 auto const & method = resolved.method();
462 auto const scale = iterationScale(covarianceLowerOf(prior), Scalar(method.tolerance));
463 ReadVector const readMean = read.mean();
464 Difference delta = Difference::Zero(prior.differenceDimension());
465 ReadDifference readStep = ReadDifference::Zero(reads.differenceIndices().size());
466 std::size_t iteration = 0;
467 bool converged = false;
468 auto const considered = consideredRows<R>(prior.space());
469 constexpr bool Considers = R::ConsiderType::Count > 0;
470
471 // A sigma-point method spreads its points by the posterior covariance of the blocks read at the
472 // iterate, in the iterate's chart, the prior's at first [sigma-point-sets]. A linear method reads no
473 // covariance, so it keeps none.
474 using Inner = std::remove_cvref_t<decltype(method.method)>;
475 constexpr bool Spreads = !isLinearMethod<Inner>;
476 using ReadMatrix = MatrixOf<
477 Scalar,
478 Reads::DifferenceSize,
479 Reads::DifferenceSize,
480 Reads::DifferenceMaxSize,
481 Reads::DifferenceMaxSize>;
482 using SpreadMatrix = std::conditional_t<Spreads, ReadMatrix, std::monostate>;
483 auto priorReadCov = SpreadMatrix{};
484 auto spreadCov = SpreadMatrix{};
485 if constexpr (Spreads)
486 {
487 priorReadCov = read.covarianceLower();
488 spreadCov = priorReadCov;
489 }
490
491 // The linearization at the iterate mu + delta, the prior mean at first, with its moments about the
492 // prior (linearizeReadsAt()): by the inner method, Linearized at the iterate or a sigma-point method
493 // over its posterior, spread by spreadCov.
494 auto const atIterate = [&](auto const & use) -> decltype(auto) {
495 return linearizeReadsAt<Space, Scalar>(
496 resolved,
497 method.method,
498 reads,
499 read,
500 f,
501 stateRead,
502 readMean,
503 iteration > 0,
504 readStep,
505 spreadCov,
506 use
507 );
508 };
509
510 // The Kalman step from the prior under one linearization: delta = K r, with K and r as
511 // conditionOnPrediction() forms them for noise of one component. For a sigma-point method it
512 // also sets spreadCov to the posterior covariance of the blocks read under that linearization,
513 // P_SS - C_S S^-1 C_S^T with C_S the rows of the cross-covariance at them, carried to the
514 // next iterate's chart. An S that is not positive definite is repaired as the measurement says,
515 // and one that stays so ends the iteration with its error (decisions.md [innovation-cholesky]).
516 auto const step =
517 [&](auto const & y, auto const & cross, auto const &) -> std::expected<Difference, GaussianError> {
518 using MeasurementSpace = std::remove_cvref_t<decltype(y.space())>;
520 using InnovationMatrix = typename GaussianParams<MeasurementSpace, Scalar>::MatrixType;
521 auto [residual, cov] = [&] {
522 if constexpr (!R::HasAdditiveNoise)
523 {
524 return std::pair{
525 observedResidual<Scalar>(resolved, y.space(), y.mean()),
526 InnovationMatrix{y.covarianceLower()}
527 };
528 }
529 else
530 {
531 auto const & noise = resolved.noise().distribution();
532 return std::pair{
533 InnovationVector{
534 observedResidual<Scalar>(resolved, y.space(), Emrys::plus(y.space(), y.mean(), noise.mean()))
535 },
536 innovationCovariancePlus<InnovationMatrix>(y, noise.covarianceLower())
537 };
538 }
539 }();
540 auto const factored = innovationCholesky<typename R::InnovationRepair>(cov);
541 if (!factored)
542 {
543 return std::unexpected{factored.error()};
544 }
545 auto const & llt = *factored;
546 auto next = Difference{};
547 next.resize(delta.size());
548 next.noalias() = multiply(cross, llt.solve(residual));
549 if constexpr (Considers)
550 {
551 zeroConsideredRows(next, considered);
552 }
553 if constexpr (Spreads)
554 {
555 auto const readCross = std::as_const(cross)(reads.differenceIndices(), Eigen::placeholders::all);
556 spreadCov = priorReadCov;
557 using ReadCross = internal::MatrixOf<
558 Scalar,
559 Reads::DifferenceSize,
560 InnovationMatrix::RowsAtCompileTime,
561 Reads::DifferenceMaxSize,
562 InnovationMatrix::MaxRowsAtCompileTime>;
563 spreadCov.noalias() -=
564 multiply(internal::transposedSolve<ReadCross>(llt, readCross.transpose()), readCross.transpose());
565 spreadCov = (Scalar(0.5) * (spreadCov + spreadCov.transpose())).eval();
566 if constexpr (Considers)
567 {
568 // The considered blocks the model reads keep their covariance with each other.
569 auto const & rows = reads.differenceIndices();
570 auto const isConsidered = [&](Eigen::Index row) {
571 for (BlockRows const block : considered)
572 {
573 if (row >= block.start && row < block.start + block.size)
574 {
575 return true;
576 }
577 }
578 return false;
579 };
580 for (Eigen::Index i = 0; i < rows.size(); ++i)
581 {
582 for (Eigen::Index j = 0; j <= i; ++j)
583 {
584 if (isConsidered(rows(i)) && isConsidered(rows(j)))
585 {
586 spreadCov(i, j) = priorReadCov(i, j);
587 spreadCov(j, i) = priorReadCov(i, j);
588 }
589 }
590 }
591 }
593 {
594 ReadDifference const nextReadStep = next(reads.differenceIndices());
595 auto const reset = resetJacobian(reads.space(), readMean, nextReadStep);
596 spreadCov = multiply(multiply(reset, spreadCov), reset.transpose()).eval();
597 }
598 }
599 return next;
600 };
601
602 // The update under the last linearization, reporting the iterations.
603 // A step that failed gives its error instead. The error is passed through finish, whose type
604 // MSVC can't name with decltype, since atIterate() holds a lambda.
605 auto failure = std::optional<GaussianError>{};
606 auto const finish = [&](auto const & y, auto const & cross, auto const & readJacobian) {
607 using Result = decltype(conditionOnPrediction<Statistics>(prior, resolved, y, cross, readJacobian, sink));
608 if (failure)
609 {
610 return Result{std::unexpected{*failure}};
611 }
612 auto result = conditionOnPrediction<Statistics>(prior, resolved, y, cross, readJacobian, sink);
613 if (result)
614 {
615 for (auto & branch : result->branches)
616 {
617 branch.iterations = iteration + 1;
618 branch.converged = converged;
619 }
620 }
621 return result;
622 };
623
624 // A step that moved too little to count stops the iteration at the linearization after it.
625 for (; iteration + 1 < method.maxIterations && !converged; ++iteration)
626 {
627 auto const next = atIterate(step);
628 if (!next)
629 {
630 failure = next.error();
631 break;
632 }
633 converged = withinScale(*next, delta, scale);
634 delta = *next;
635 readStep = delta(reads.differenceIndices());
636 }
637 return atIterate(finish);
638}
639
640} // namespace internal
641
642} // namespace Emrys
The allocator conventions of decisions.md [allocation]: DefaultAllocator, Capacity,...
Spaces built from other spaces (decisions.md [sizes-and-dimensions], [several-resizable-blocks]): Com...
The coordinate types (decisions.md [no-bounded-coordinates]): Linear, plain coordinates,...
Execution policies (decisions.md [execution-policies]): execution::sequential, the ExecutionPolicy co...
The Gaussian: its parameters GaussianParams, the Gaussian that evaluates the density,...
What marginals and conditionals are built from: resolving Eigen index sets against a space; the share...
The macros every header includes first.
Measurement models and their observations (decisions.md [model-policies], [sensors],...
How an update reads a measurement, all internal (decisions.md [model-policies], [measurement-space-fu...
The mixture: the parameters MixtureParams of weighted components of one type (decisions....
Products of fixed-size matrices by Eigen's fixed-size product at every size: multiply() is by Eigen'...
Product: the product of independent densities, each on its own block of a space, built with on<Path>(...
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...
Subspaces, and growing and shrinking blocks (decisions.md [several-resizable-blocks]).
Tag paths and the index utilities (decisions.md [tag-addressing], [index-names]): TagPath and TagPath...
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...
typename MomentsType::MatrixType MatrixType
a matrix over differences
Definition Gaussian.h:267
typename MomentsType::DifferenceVectorType DifferenceVectorType
a difference, such as a residual
Definition Gaussian.h:266
constexpr Residual< Inner > residual(Inner inner={})
Definition Resampling.h:544
auto plus(S const &space, Eigen::MatrixBase< XDerived > const &x, Eigen::MatrixBase< DDerived > const &d)
Definition Space.h:675
auto block(Space auto const &space, X &&x)
Definition TagPath.h:979
auto resetJacobian(S const &space, Eigen::MatrixBase< XDerived > const &x, Eigen::MatrixBase< DDerived > const &d)
Definition Reset.h:377
constexpr bool hasIdentityReset
Whether the reset Jacobian of S is the identity, so that a covariance needs no change when its mean m...
Definition Reset.h:100
constexpr Linearized linearized()
Definition Methods.h:333