EmrysFilter 0.1.0
State estimation utilities in C++23 using Eigen
Loading...
Searching...
No Matches
Categorical.h
Go to the documentation of this file.
1#pragma once
2
13
14#include <array>
15#include <concepts>
16#include <cstddef>
17#include <expected>
18#include <random>
19#include <utility>
20#include <vector>
21
32
33namespace Emrys
34{
35
36template <DiscreteSpace Space_, RealScalar Scalar_>
38
39template <DiscreteSpace Space_, RealScalar Scalar_>
40class Categorical;
41
47enum class ChainError
48{
50};
51
52namespace internal
53{
54
61template <typename Scalar, typename Matrix, typename DwellTimes, typename ExitWeights>
62[[nodiscard]] Matrix
63dwellGenerator(Eigen::MatrixBase<DwellTimes> const & dwellTimes, Eigen::MatrixBase<ExitWeights> const & exitWeights)
64{
65 Eigen::Index const n = exitWeights.rows();
66 EMRYS_FILTER_ASSERT(
67 dwellTimes.size() == n && exitWeights.cols() == n && "give a dwell time and a row of exit weights per value"
68 );
69 auto generator = Matrix{};
70 generator.resize(n, n);
71 for (Eigen::Index i = 0; i < n; ++i)
72 {
73 Scalar const dwell = Scalar(dwellTimes(i));
74 EMRYS_FILTER_ASSERT(dwell > Scalar(0) && "a dwell time must be positive");
75 EMRYS_FILTER_ASSERT(exitWeights(i, i) == 0 && "a value's weight of exiting to itself must be zero");
76 EMRYS_FILTER_ASSERT((exitWeights.row(i).array() >= 0).all() && "exit weights must be nonnegative");
77 Scalar const leave = Scalar(1) / dwell;
78 Scalar const total = Scalar(exitWeights.row(i).sum());
79 EMRYS_FILTER_ASSERT(
80 (leave == Scalar(0) || total > Scalar(0)) && "a value that is left needs a value to exit to"
81 );
82 if (leave == Scalar(0))
83 {
84 generator.row(i).setZero();
85 }
86 else
87 {
88 generator.row(i) = exitWeights.row(i).template cast<Scalar>() * Scalar(leave / total);
89 }
90 generator(i, i) = -leave;
91 }
92 return generator;
93}
94
108template <typename Scalar, typename Array, typename Matrix>
109[[nodiscard]] std::expected<Array, ChainError>
110stationaryProbabilities(Matrix kernel)
111{
112 Eigen::Index const n = kernel.rows();
113 EMRYS_FILTER_ASSERT(n > 0 && "a chain has at least one value");
114 for (Eigen::Index k = n - 1; k > 0; --k)
115 {
116 // Watched only at the values before k, the chain moves from i to j directly or through
117 // k, which it leaves for an earlier value with the total of row k before it.
118 Scalar const leave = kernel.row(k).head(k).sum();
119 if (!(leave > Scalar(0)))
120 {
121 return std::unexpected{ChainError::Reducible};
122 }
123 kernel.col(k).head(k) /= leave;
124 kernel.topLeftCorner(k, k).noalias() += kernel.col(k).head(k) * kernel.row(k).head(k);
125 }
126 Array probabilities = Array::Zero(n);
127 probabilities(0) = Scalar(1);
128 for (Eigen::Index k = 1; k < n; ++k)
129 {
130 Scalar const flow = probabilities.head(k).matrix().dot(kernel.col(k).head(k));
131 if (!(flow > Scalar(0)))
132 {
133 return std::unexpected{ChainError::Reducible};
134 }
135 probabilities(k) = flow;
136 }
137 probabilities /= probabilities.sum();
138 return probabilities;
139}
140
146template <typename P, typename Scalar, DiscreteSpace Space, typename Values>
147[[nodiscard]] auto
148categoricalMarginal(Space const & space, Values const & logValues)
149{
150 auto const [stride, axis] = locateAxisByPath<P>(space);
151 using Axis = std::remove_cvref_t<decltype(*axis)>;
152 using AxisArray = typename CategoricalParams<Axis, Scalar>::ArrayType;
153 Eigen::Index const count = axis->cardinality();
154 // A LogSumExp per value of the axis, kept as its running maximum and sum in the axis's own
155 // storage, so a bounded axis allocates nothing.
156 AxisArray maxima = AxisArray::Constant(count, -Eigen::NumTraits<Scalar>::infinity());
157 AxisArray sums = AxisArray::Zero(count);
158 for (Eigen::Index v = 0; v < logValues.size(); ++v)
159 {
160 Eigen::Index const i = (v / stride) % count;
161 Scalar const x = logValues(v);
162 if (x == -Eigen::NumTraits<Scalar>::infinity())
163 {
164 continue;
165 }
166 if (x <= maxima(i))
167 {
168 sums(i) += Eigen::numext::exp(x - maxima(i));
169 }
170 else
171 {
172 sums(i) = sums(i) * Eigen::numext::exp(maxima(i) - x) + Scalar(1);
173 maxima(i) = x;
174 }
175 }
176 for (Eigen::Index i = 0; i < count; ++i)
177 {
178 maxima(i) =
179 sums(i) == Scalar(0) ? -Eigen::NumTraits<Scalar>::infinity() : maxima(i) + Eigen::numext::log(sums(i));
180 }
181 return CategoricalParams<Axis, Scalar>{*axis, maxima};
182}
183
188template <typename Scalar, DiscreteSpace Rest, typename Values>
189[[nodiscard]] CategoricalParams<Rest, Scalar>
190categoricalConditional(Rest rest, ValueLocation const & location, Values const & logValues)
191{
192 using RestArray = typename CategoricalParams<Rest, Scalar>::ArrayType;
193 auto selected = RestArray{};
194 selected.resize(rest.cardinality());
195 Eigen::Index next = 0;
196 for (Eigen::Index v = 0; v < logValues.size(); ++v)
197 {
198 if (location.matches(v))
199 {
200 selected(next++) = logValues(v);
201 }
202 }
203 EMRYS_FILTER_ASSERT(next == selected.size());
204 return CategoricalParams<Rest, Scalar>{std::move(rest), selected};
205}
206
212template <typename Scalar, typename Array, typename Kernel>
213[[nodiscard]] Array
214movedLogWeights(Array const & logWeights, Kernel const & kernel)
215{
216 // Every discrete space has a value, so there is a largest log-weight.
217 Scalar const largest = logWeights.maxCoeff();
218 Scalar const shift = isFinite(largest) ? largest : Scalar(0);
219 Array const weights = (logWeights - shift).exp();
220 Array moved = multiply(kernel.transpose(), weights.matrix()).array();
221 return moved.log() + shift;
222}
223
224} // namespace internal
225
255template <DiscreteSpace Space_, RealScalar Scalar_ = double>
257{
258public:
259
260 using Scalar = Scalar_;
261 using SpaceType = Space_;
263 static constexpr int CardinalityAtCompileTime = Space_::Cardinality;
264 static constexpr int MaxCardinalityAtCompileTime = Space_::MaxCardinality;
265 static constexpr bool IsParams = true;
266
268 using ArrayType =
269 Eigen::Array<Scalar_, CardinalityAtCompileTime, 1, Eigen::ColMajor, MaxCardinalityAtCompileTime, 1>;
270
272 explicit CategoricalParams(Space_ space = {})
273 : m_space{std::move(space)}
274 , m_logWeights{ArrayType::Constant(m_space.cardinality(), -Eigen::numext::log(Scalar(m_space.cardinality())))}
275 {}
276
280 template <typename Derived>
281 CategoricalParams(Space_ space, Eigen::DenseBase<Derived> const & logWeights)
282 : m_space{std::move(space)}
283 , m_logWeights{logWeights.derived().array().template cast<Scalar>()}
284 {
285 EMRYS_FILTER_ASSERT(m_logWeights.size() == m_space.cardinality() && "give one log-weight per value");
286 }
287
292 template <typename Derived>
293 [[nodiscard]] static CategoricalParams
294 fromWeights(Space_ space, Eigen::DenseBase<Derived> const & weights)
295 {
296 return CategoricalParams{std::move(space), weights.derived().array().template cast<Scalar>().log()};
297 }
298
302 template <typename Derived>
304 [[nodiscard]] static CategoricalParams
305 fromWeights(Eigen::DenseBase<Derived> const & weights)
306 {
307 return fromWeights(Space_{}, weights);
308 }
309
315 template <typename... Weights>
316 requires FixedSizeSpace<Space_> && (CardinalityAtCompileTime != Eigen::Dynamic) && (sizeof...(Weights) > 0)
317 && (std::convertible_to<Weights, Scalar_> && ...)
318 [[nodiscard]] static CategoricalParams
319 fromWeights(Weights... weights)
320 {
321 static_assert(sizeof...(Weights) == CardinalityAtCompileTime, "give one weight per value");
322 auto array = ArrayType{};
323 Eigen::Index i = 0;
324 ((array(i++) = Scalar_(weights)), ...);
325 return fromWeights(Space_{}, array);
326 }
327
329 [[nodiscard]] Space_ const &
330 space() const
331 {
332 return m_space;
333 }
334
336 [[nodiscard]] Eigen::Index
338 {
339 return m_logWeights.size();
340 }
341
343 [[nodiscard]] ArrayType const &
345 {
346 return m_logWeights;
347 }
348
350 [[nodiscard]] Scalar
351 logWeight(Eigen::Index value) const
352 {
353 EMRYS_FILTER_ASSERT(value >= 0 && value < cardinality() && "the value is out of range");
354 return m_logWeights(value);
355 }
356
360 [[nodiscard]] std::expected<Scalar, WeightError>
362 {
363 return internal::logTotalWeight<Scalar>(m_logWeights);
364 }
365
367 template <typename Derived>
368 void
369 setLogWeights(Eigen::DenseBase<Derived> const & logWeights)
370 {
371 EMRYS_FILTER_ASSERT(logWeights.size() == cardinality() && "give one log-weight per value");
372 m_stamp.touchValues();
373 m_logWeights = logWeights.derived().array().template cast<Scalar>();
374 }
375
377 template <typename Derived>
378 void
379 setWeights(Eigen::DenseBase<Derived> const & weights)
380 {
381 setLogWeights(weights.derived().array().template cast<Scalar>().log());
382 }
383
385 void
386 setLogWeight(Eigen::Index value, Scalar logWeight)
387 {
388 EMRYS_FILTER_ASSERT(value >= 0 && value < cardinality() && "the value is out of range");
389 m_stamp.touchValues();
390 m_logWeights(value) = logWeight;
391 }
392
394 void
395 setWeight(Eigen::Index value, Scalar weight)
396 {
397 setLogWeight(value, Scalar(Eigen::numext::log(weight)));
398 }
399
403 template <typename... Values>
404 requires(sizeof...(Values) > 0)
405 void
410
414 template <typename... Values>
415 requires(sizeof...(Values) > 0)
416 void
418 {
419 setWeight(valueOf<Values...>(m_space), weight);
420 }
421
426 template <typename Derived>
427 void
428 reweight(Eigen::DenseBase<Derived> const & logLikelihoods)
429 {
430 EMRYS_FILTER_ASSERT(logLikelihoods.size() == cardinality() && "give one log-likelihood per value");
431 m_stamp.touchValues();
432 m_logWeights += logLikelihoods.derived().array().template cast<Scalar>();
433 }
434
442 template <typename Derived>
443 [[nodiscard]] std::expected<Scalar, WeightError>
444 update(Eigen::DenseBase<Derived> const & logLikelihoods)
445 {
446 EMRYS_FILTER_ASSERT(logLikelihoods.size() == cardinality() && "give one log-likelihood per value");
447 auto const prior = logTotalWeight();
448 if (!prior)
449 {
450 return std::unexpected{prior.error()};
451 }
452 ArrayType posterior = m_logWeights + logLikelihoods.derived().array().template cast<Scalar>();
453 auto const total = internal::logTotalWeight<Scalar>(posterior);
454 if (!total)
455 {
456 return std::unexpected{total.error()};
457 }
458 m_stamp.touchValues();
459 m_logWeights = posterior - *total;
460 return *total - *prior;
461 }
462
466 [[nodiscard]] std::expected<void, WeightError>
468 {
469 auto const total = logTotalWeight();
470 if (!total)
471 {
472 return std::unexpected{total.error()};
473 }
474 m_stamp.touchValues();
475 m_logWeights -= *total;
476 return {};
477 }
478
482 template <typename P>
483 [[nodiscard]] auto
484 marginal() const
485 {
486 return internal::categoricalMarginal<P, Scalar>(m_space, m_logWeights);
487 }
488
493 template <typename P>
494 [[nodiscard]] auto
495 conditional(Eigen::Index value) const
496 {
497 auto const [stride, axis] = internal::locateAxisByPath<P>(m_space);
498 EMRYS_FILTER_ASSERT(value >= 0 && value < axis->cardinality() && "the value is out of range");
499 return internal::categoricalConditional<Scalar>(
500 internal::complementAxes<P>(m_space),
501 internal::ValueLocation{stride, axis->cardinality(), value},
502 m_logWeights
503 );
504 }
505
510 template <typename Value>
511 [[nodiscard]] auto
513 {
514 return internal::categoricalConditional<Scalar>(
515 internal::complementAxes<internal::ValueAxisPath<Value, Space_>>(m_space),
516 internal::locateValuePath<Value>(m_space),
517 m_logWeights
518 );
519 }
520
524 [[nodiscard]] std::expected<DensityType, WeightError>
525 toDensity() const &
526 {
527 auto const total = logTotalWeight();
528 if (!total)
529 {
530 return std::unexpected{total.error()};
531 }
532 return DensityType{*this, *total};
533 }
534
536 void
537 toDensity() const && = delete;
538
539private:
540
541 friend DensityType;
542
544 template <typename>
545 friend class internal::ValuesCheck;
546
547 EMRYS_FILTER_NO_UNIQUE_ADDRESS Space_ m_space;
548 ArrayType m_logWeights;
549 internal::Stamp m_stamp;
550};
551
553template <DiscreteSpace Space, typename Derived>
554CategoricalParams(Space, Eigen::DenseBase<Derived> const &) -> CategoricalParams<Space, typename Derived::Scalar>;
555
557
558namespace internal
559{
560
566template <typename Space, typename Scalar>
567[[nodiscard]] auto
568logProbabilitiesOf(CategoricalParams<Space, Scalar> const & params)
569{
570 return params.logWeights() - params.logTotalWeight().value_or(Eigen::NumTraits<Scalar>::quiet_NaN());
571}
572
573} // namespace internal
574
599template <DiscreteSpace Space_, RealScalar Scalar_ = double>
600class Categorical
601{
602public:
603
605 using Scalar = Scalar_;
606 using SpaceType = Space_;
608 static constexpr int CardinalityAtCompileTime =
610 static constexpr int MaxCardinalityAtCompileTime =
612 static constexpr bool IsParams = false;
613
615 [[nodiscard]] Space_ const &
616 space() const
617 {
618 return source().space();
619 }
620
622 [[nodiscard]] Eigen::Index
624 {
625 return source().cardinality();
626 }
627
631 [[nodiscard]] auto
633 {
634 return source().m_logWeights - m_logTotal;
635 }
636
640 [[nodiscard]] auto
642 {
643 return logProbabilities().exp();
644 }
645
647 [[nodiscard]] Scalar
648 logProbability(Eigen::Index value) const
649 {
650 return source().logWeight(value) - m_logTotal;
651 }
652
654 [[nodiscard]] Scalar
655 probability(Eigen::Index value) const
656 {
657 return Eigen::numext::exp(logProbability(value));
658 }
659
664 template <typename... Values>
665 requires(sizeof...(Values) > 0)
666 [[nodiscard]] Scalar
668 {
669 ParamsType const & params = source();
670 auto const locations = std::array<internal::ValueLocation, sizeof...(Values)>{
671 internal::locateValuePath<Values>(params.m_space)...
672 };
673 auto total = LogSumExp<Scalar>{};
674 for (Eigen::Index v = 0; v < params.cardinality(); ++v)
675 {
676 bool matches = true;
677 for (auto const & location : locations)
678 {
679 matches = matches && location.matches(v);
680 }
681 if (matches)
682 {
683 total.add(params.m_logWeights(v));
684 }
685 }
686 return total.value() - m_logTotal;
687 }
688
691 template <typename... Values>
692 requires(sizeof...(Values) > 0)
693 [[nodiscard]] Scalar
695 {
696 return Eigen::numext::exp(logProbability<Values...>());
697 }
698
708 [[nodiscard]] Scalar
709 entropy() const
710 {
711 ArrayType const & logWeights = source().m_logWeights;
712 Scalar result = 0;
713 for (Eigen::Index v = 0; v < logWeights.size(); ++v)
714 {
715 if (logWeights(v) != -Eigen::NumTraits<Scalar>::infinity())
716 {
717 Scalar const logProbability = logWeights(v) - m_logTotal;
718 result -= Eigen::numext::exp(logProbability) * logProbability;
719 }
720 }
721 return Eigen::numext::maxi(result, Scalar(0));
722 }
723
725 [[nodiscard]] Eigen::Index
727 {
728 ArrayType const & logWeights = source().m_logWeights;
729 Eigen::Index best = 0;
730 for (Eigen::Index v = 1; v < logWeights.size(); ++v)
731 {
732 if (logWeights(v) > logWeights(best))
733 {
734 best = v;
735 }
736 }
737 return best;
738 }
739
743 template <typename P>
744 [[nodiscard]] auto
745 marginal() const
746 {
747 return internal::categoricalMarginal<P, Scalar>(space(), logProbabilities());
748 }
749
753 template <typename P>
754 [[nodiscard]] auto
755 conditional(Eigen::Index value) const
756 {
757 return source().template conditional<P>(value);
758 }
759
763 template <typename Value>
764 [[nodiscard]] auto
766 {
767 return source().template conditional<Value>();
768 }
769
770private:
771
772 friend ParamsType;
773
777 Categorical(ParamsType const & source, Scalar logTotal)
778 : m_check{source}
779 , m_logTotal{logTotal}
780 {}
781
783 [[nodiscard]] ParamsType const &
784 source() const
785 {
786 return m_check.get();
787 }
788
789 internal::ValuesCheck<ParamsType> m_check;
790 Scalar m_logTotal;
791};
792
827template <DiscreteSpace Space_, RealScalar Scalar_ = double>
829{
830public:
831
832 using Scalar = Scalar_;
833 using SpaceType = Space_;
834 static constexpr int CardinalityAtCompileTime = Space_::Cardinality;
835 static constexpr int MaxCardinalityAtCompileTime = Space_::MaxCardinality;
836
838 using MatrixType = Eigen::Matrix<
839 Scalar_,
842 Eigen::RowMajor,
845
847 explicit MarkovChain(Space_ space = {})
848 : m_space{std::move(space)}
849 , m_transitions{MatrixType::Identity(m_space.cardinality(), m_space.cardinality())}
850 {}
851
855 template <MatrixExpression Transitions>
856 MarkovChain(Space_ space, Transitions && transitions)
857 : m_space{std::move(space)}
858 , m_transitions{internal::castScalar<Scalar>(std::forward<Transitions>(transitions))}
859 {
860 EMRYS_FILTER_ASSERT(
861 m_transitions.rows() == m_space.cardinality() && m_transitions.cols() == m_space.cardinality()
862 && "the transition matrix has a row and a column per value"
863 );
864 EMRYS_FILTER_ASSERT(
865 (m_transitions.array() >= Scalar(0)).all() && "transition probabilities must be nonnegative"
866 );
867 EMRYS_FILTER_ASSERT(
868 ((m_transitions.rowwise().sum().array() - Scalar(1)).abs() <= Eigen::NumTraits<Scalar>::dummy_precision())
869 .all()
870 && "each row of the transition matrix must sum to one"
871 );
872 }
873
877 template <MatrixExpression Transitions>
879 explicit MarkovChain(Transitions && transitions)
880 : MarkovChain{Space_{}, std::forward<Transitions>(transitions)}
881 {}
882
886 template <DiscreteSpace... Axes>
887 requires(
888 internal::IsDiscreteProduct<Space_>
889 && sizeof...(Axes) == std::tuple_size_v<internal::DiscreteProductAxes<Space_>>
890 )
892 : m_space{std::move(space)}
893 {
894 static_assert(
895 std::is_same_v<std::tuple<Axes...>, internal::DiscreteProductAxes<Space_>>,
896 "give one chain per axis of the product, in order"
897 );
898 Eigen::Index const n = m_space.cardinality();
899 m_transitions = MatrixType::Ones(n, n);
900 Eigen::Index stride = n;
901 (
902 [&] {
903 Eigen::Index const count = chains.space().cardinality();
904 stride /= count;
905 for (Eigen::Index i = 0; i < n; ++i)
906 {
907 for (Eigen::Index j = 0; j < n; ++j)
908 {
909 m_transitions(i, j) *= chains.matrix()((i / stride) % count, (j / stride) % count);
910 }
911 }
912 }(),
913 ...
914 );
915 EMRYS_FILTER_ASSERT(stride == 1 && "each chain must have its axis's cardinality");
916 }
917
921 template <DiscreteSpace... Axes>
922 requires(
923 FixedSizeSpace<Space_> && internal::IsDiscreteProduct<Space_>
924 && sizeof...(Axes) == std::tuple_size_v<internal::DiscreteProductAxes<Space_>>
925 )
926 explicit MarkovChain(MarkovChain<Axes, Scalar_> const &... chains)
927 : MarkovChain{Space_{}, chains...}
928 {}
929
933 [[nodiscard]] static MarkovChain
934 persistent(Scalar stay, Space_ space = {})
935 {
936 EMRYS_FILTER_ASSERT(stay >= Scalar(0) && stay <= Scalar(1) && "the probability must be in [0, 1]");
937 Eigen::Index const n = space.cardinality();
938 auto result = MarkovChain{std::move(space)};
939 if (n > 1)
940 {
941 result.m_transitions.setConstant((Scalar(1) - stay) / Scalar(n - 1));
942 result.m_transitions.diagonal().setConstant(stay);
943 }
944 return result;
945 }
946
950 [[nodiscard]] static MarkovChain
952 {
953 auto result = MarkovChain{next.space()};
954 result.m_transitions = next.probabilities().matrix().transpose().replicate(next.cardinality(), 1);
955 return result;
956 }
957
971 template <typename DwellTimes, typename ExitWeights>
972 [[nodiscard]] static MarkovChain
974 Eigen::MatrixBase<DwellTimes> const & dwellTimes,
975 Eigen::MatrixBase<ExitWeights> const & exitWeights,
976 Space_ space = {}
977 )
978 {
979 EMRYS_FILTER_ASSERT((dwellTimes.array() >= 1).all() && "a dwell time is at least one step");
980 MatrixType transitions = internal::dwellGenerator<Scalar, MatrixType>(dwellTimes, exitWeights);
981 transitions.diagonal().array() += Scalar(1);
982 EMRYS_FILTER_ASSERT(transitions.rows() == space.cardinality() && "give a dwell time per value");
983 return MarkovChain{std::move(space), std::move(transitions)};
984 }
985
990 template <typename DwellTimes>
991 [[nodiscard]] static MarkovChain
992 fromDwellTimes(Eigen::MatrixBase<DwellTimes> const & dwellTimes, Space_ space = {})
993 {
994 Eigen::Index const n = space.cardinality();
995 return fromDwellTimes(dwellTimes, MatrixType::Ones(n, n) - MatrixType::Identity(n, n), std::move(space));
996 }
997
999 [[nodiscard]] Space_ const &
1000 space() const
1001 {
1002 return m_space;
1003 }
1004
1006 [[nodiscard]] MatrixType const &
1007 matrix() const
1008 {
1009 return m_transitions;
1010 }
1011
1013 [[nodiscard]] Scalar
1014 transition(Eigen::Index from, Eigen::Index to) const
1015 {
1016 return m_transitions(from, to);
1017 }
1018
1024 next(Eigen::Index from) const
1025 {
1026 EMRYS_FILTER_ASSERT(from >= 0 && from < m_space.cardinality() && "the value is not in the chain's space");
1027 return CategoricalParams<Space_, Scalar_>{m_space, m_transitions.row(from).transpose().array().log()};
1028 }
1029
1030private:
1031
1032 Space_ m_space;
1033 MatrixType m_transitions;
1034};
1035
1037template <DiscreteSpace Space, MatrixExpression Transitions>
1038MarkovChain(Space, Transitions &&) -> MarkovChain<Space, typename std::remove_cvref_t<Transitions>::Scalar>;
1039
1041
1047template <typename Scalar, typename Space>
1048[[nodiscard]] CategoricalParams<Space, Scalar>
1050{
1051 EMRYS_FILTER_ASSERT(prior.space() == chain.space() && "the chain is on a different space");
1053 prior.space(),
1054 internal::movedLogWeights<Scalar>(prior.logWeights(), chain.matrix())
1055 };
1056}
1057
1067template <typename Scalar, typename Space>
1068[[nodiscard]] std::expected<CategoricalParams<Space, Scalar>, ChainError>
1070{
1072 std::expected<ArrayType, ChainError> const probabilities =
1073 internal::stationaryProbabilities<Scalar, ArrayType>(chain.matrix());
1074 if (!probabilities)
1075 {
1076 return std::unexpected{probabilities.error()};
1077 }
1078 return CategoricalParams<Space, Scalar>{chain.space(), probabilities->log()};
1079}
1080
1085template <typename Scalar, typename Space, std::uniform_random_bit_generator G>
1086[[nodiscard]] std::vector<Eigen::Index>
1087drawSamples(Categorical<Space, Scalar> const & distribution, Eigen::Index n, G & rng)
1088{
1089 static_assert(std::floating_point<Scalar>, "sampling requires a standard floating-point scalar");
1090 EMRYS_FILTER_ASSERT(n >= 0);
1091 typename Categorical<Space, Scalar>::ArrayType const probabilities = distribution.probabilities();
1092 auto draw =
1093 std::discrete_distribution<Eigen::Index>{probabilities.data(), probabilities.data() + probabilities.size()};
1094 std::size_t const count = std::size_t(n);
1095 auto samples = std::vector<Eigen::Index>(count);
1096 for (auto & sample : samples)
1097 {
1098 sample = draw(rng);
1099 }
1100 return samples;
1101}
1102
1109template <typename Scalar, typename Space, std::uniform_random_bit_generator G>
1110[[nodiscard]] Eigen::Index
1111drawSample(Categorical<Space, Scalar> const & distribution, G & rng)
1112{
1113 auto const probabilities = distribution.probabilities();
1114 return internal::drawIndex<Scalar>(
1115 probabilities.size(),
1116 [&](Eigen::Index i) {
1117 return probabilities(i);
1118 },
1119 rng
1120 );
1121}
1122
1123} // namespace Emrys
1124
1125#ifndef EMRYS_FILTER_NO_IO
1126
1127namespace Emrys
1128{
1129
1130namespace internal
1131{
1132
1134template <typename Space_, typename Scalar_>
1135void
1136describe(std::ostream & stream, Categorical<Space_, Scalar_> const & categorical, Indent indent)
1137{
1138 typename Categorical<Space_, Scalar_>::ArrayType const probabilities = categorical.probabilities();
1139 stream << indent << "categorical over ";
1140 printCount(stream, probabilities.size(), "value");
1141 stream << '\n';
1142 printRow(stream, indent.deeper(), "probabilities", probabilities);
1143}
1144
1148template <typename Space_, typename Scalar_>
1149void
1150describe(std::ostream & stream, CategoricalParams<Space_, Scalar_> const & params, Indent indent)
1151{
1152 if (auto const density = params.toDensity())
1153 {
1154 describe(stream, *density, indent);
1155 return;
1156 }
1157 stream << indent << "categorical over ";
1158 printCount(stream, params.cardinality(), "value");
1159 stream << ", whose weights are not normalizable\n";
1160 printRow(stream, indent.deeper(), "log-weights", params.logWeights());
1161}
1162
1163} // namespace internal
1164
1165} // namespace Emrys
1166
1167#endif // EMRYS_FILTER_NO_IO
Discrete spaces (decisions.md [discrete-spaces]): the DiscreteSpace concept; Finite,...
How a function that stores a matrix argument takes it (decisions.md [forwarded-matrices]): the concep...
The macros every header includes first.
Mathematical constants and scalar functions that work in every scalar type the library accepts,...
Products of fixed-size matrices by Eigen's fixed-size product at every size: multiply() is by Eigen'...
Params and Density: the traits that pair a distribution's parameters with its density,...
Printing: operator<< and std::formatter for every distribution, every filter (which prints its name a...
The Space concept and what every continuous space shares (decisions.md [sizes-and-dimensions]): the t...
What parameters and their views share (decisions.md [view-stamp], [params-and-density]): Stamp,...
Log-weights: the WeightError a normalization can fail with (NumericError.h), and the streaming log-su...
The parameters of a probability distribution over a discrete space: one weight per value.
Definition Categorical.h:257
CategoricalParams(Space_ space={})
The uniform distribution over space, with log-weights that are its log-probabilities.
Definition Categorical.h:272
auto conditional() const
Definition Categorical.h:512
static constexpr bool IsParams
Definition Categorical.h:265
Space_ SpaceType
the discrete space
Definition Categorical.h:261
void setLogWeights(Eigen::DenseBase< Derived > const &logWeights)
Sets the log-weights to logWeights, one per value.
Definition Categorical.h:369
auto marginal() const
Definition Categorical.h:484
void setWeight(Eigen::Index value, Scalar weight)
Sets the weight of the value with flat index value to weight.
Definition Categorical.h:395
std::expected< Scalar, WeightError > update(Eigen::DenseBase< Derived > const &logLikelihoods)
Bayes' rule: multiplies each value's probability by its likelihood , with logLikelihoods one per val...
Definition Categorical.h:444
static constexpr int MaxCardinalityAtCompileTime
Definition Categorical.h:264
void setWeights(Eigen::DenseBase< Derived > const &weights)
Sets the weights to weights, one per value, which need not sum to one.
Definition Categorical.h:379
void toDensity() const &&=delete
A density of a temporary would outlive it.
ArrayType const & logWeights() const
Definition Categorical.h:344
void reweight(Eigen::DenseBase< Derived > const &logLikelihoods)
Multiplies each value's weight by its likelihood , with logLikelihoods one per value,...
Definition Categorical.h:428
std::expected< DensityType, WeightError > toDensity() const &
Definition Categorical.h:525
Space_ const & space() const
Definition Categorical.h:330
static CategoricalParams fromWeights(Space_ space, Eigen::DenseBase< Derived > const &weights)
Definition Categorical.h:294
CategoricalParams(Space_ space, Eigen::DenseBase< Derived > const &logWeights)
The distribution over space whose probabilities are proportional to , with logWeights ,...
Definition Categorical.h:281
static constexpr int CardinalityAtCompileTime
Definition Categorical.h:263
auto conditional(Eigen::Index value) const
Definition Categorical.h:495
Categorical< Space_, Scalar_ > DensityType
the view toDensity() returns
Definition Categorical.h:262
void setLogWeight(Scalar logWeight)
Sets the log-weight of the value with the named values Values, one per axis (see valueOf()),...
Definition Categorical.h:406
static CategoricalParams fromWeights(Weights... weights)
Definition Categorical.h:319
Eigen::Index cardinality() const
Definition Categorical.h:337
void setWeight(Scalar weight)
Sets the weight of the value with the named values Values, one per axis (see valueOf()),...
Definition Categorical.h:417
static CategoricalParams fromWeights(Eigen::DenseBase< Derived > const &weights)
Definition Categorical.h:305
std::expected< void, WeightError > normalize()
Shifts the log-weights so that the weights sum to one, or returns the WeightError that stops it,...
Definition Categorical.h:467
Scalar logWeight(Eigen::Index value) const
Definition Categorical.h:351
std::expected< Scalar, WeightError > logTotalWeight() const
Definition Categorical.h:361
Scalar_ Scalar
the scalar type of the weights
Definition Categorical.h:260
void setLogWeight(Eigen::Index value, Scalar logWeight)
Sets the log-weight of the value with flat index value to logWeight.
Definition Categorical.h:386
Eigen::Array< Scalar_, CardinalityAtCompileTime, 1, Eigen::ColMajor, MaxCardinalityAtCompileTime, 1 > ArrayType
One entry per value, in flat-index order.
Definition Categorical.h:268
The probabilities of a CategoricalParams whose weights define a distribution: a read-only view.
Definition Categorical.h:601
auto conditional() const
Definition Categorical.h:765
auto logProbabilities() const
Definition Categorical.h:632
Space_ const & space() const
Definition Categorical.h:616
Space_ SpaceType
Definition Categorical.h:606
typename ParamsType::ArrayType ArrayType
Definition Categorical.h:607
Scalar entropy() const
Definition Categorical.h:709
auto marginal() const
Definition Categorical.h:745
static constexpr int MaxCardinalityAtCompileTime
Definition Categorical.h:610
CategoricalParams< Space_, Scalar_ > ParamsType
Definition Categorical.h:604
static constexpr bool IsParams
Definition Categorical.h:612
Eigen::Index mostProbable() const
Definition Categorical.h:726
auto probabilities() const
Definition Categorical.h:641
Scalar_ Scalar
Definition Categorical.h:605
Scalar logProbability() const
Definition Categorical.h:667
auto conditional(Eigen::Index value) const
Definition Categorical.h:755
Eigen::Index cardinality() const
Definition Categorical.h:623
Scalar logProbability(Eigen::Index value) const
Definition Categorical.h:648
static constexpr int CardinalityAtCompileTime
Definition Categorical.h:608
Scalar probability() const
Definition Categorical.h:694
Scalar probability(Eigen::Index value) const
Definition Categorical.h:655
The block named by Path does not change, and has no process noise, such as a fixed calibration parame...
Definition ProcessModel.h:1274
Streaming log-sum-exp: accumulates without overflow.
Definition Weights.h:46
Scalar value() const
Definition Weights.h:133
A Markov chain on a discrete space: the probability of moving from each value to each value in one st...
Definition Categorical.h:829
Eigen::Matrix< Scalar_, CardinalityAtCompileTime, CardinalityAtCompileTime, Eigen::RowMajor, MaxCardinalityAtCompileTime, MaxCardinalityAtCompileTime > MatrixType
The transition matrix's type: a row per value moved from, a column per value moved to.
Definition Categorical.h:838
Space_ SpaceType
the discrete space
Definition Categorical.h:833
static constexpr int CardinalityAtCompileTime
Definition Categorical.h:834
MarkovChain(Space_ space={})
The chain that never moves.
Definition Categorical.h:847
MarkovChain(Space_ space, MarkovChain< Axes, Scalar_ > const &... chains)
The chain on the DiscreteProduct space whose axes move independently, each by its own chain in chains...
Definition Categorical.h:891
Scalar transition(Eigen::Index from, Eigen::Index to) const
Definition Categorical.h:1014
CategoricalParams< Space_, Scalar_ > next(Eigen::Index from) const
Definition Categorical.h:1024
static MarkovChain persistent(Scalar stay, Space_ space={})
Definition Categorical.h:934
Space_ const & space() const
Definition Categorical.h:1000
MarkovChain(Space_ space, Transitions &&transitions)
The chain on space with transition matrix transitions: nonnegative, with each row summing to one.
Definition Categorical.h:856
Scalar_ Scalar
the scalar type of the probabilities
Definition Categorical.h:832
MarkovChain(MarkovChain< Axes, Scalar_ > const &... chains)
The chain on a DiscreteProduct that is a FixedSizeSpace, named as the template argument,...
Definition Categorical.h:926
MarkovChain(Transitions &&transitions)
The chain on a FixedSizeSpace, named as the template argument, with transition matrix transitions: Ma...
Definition Categorical.h:879
static MarkovChain fromDwellTimes(Eigen::MatrixBase< DwellTimes > const &dwellTimes, Eigen::MatrixBase< ExitWeights > const &exitWeights, Space_ space={})
Definition Categorical.h:973
MatrixType const & matrix() const
Definition Categorical.h:1007
static constexpr int MaxCardinalityAtCompileTime
Definition Categorical.h:835
static MarkovChain fromDwellTimes(Eigen::MatrixBase< DwellTimes > const &dwellTimes, Space_ space={})
Definition Categorical.h:992
static MarkovChain memoryless(Categorical< Space_, Scalar_ > const &next)
Definition Categorical.h:951
Axes: a way of taking a root of a covariance , , whose columns carry a sigma-point rule's unit point...
Definition Roots.h:300
A type usable as a discrete space: a finite set of values.
Definition Discrete.h:72
A space whose instance holds nothing, so that S{} is the only one there is: a space of fixed size,...
Definition Space.h:217
A type usable as the space of a distribution.
Definition Space.h:186
#define EMRYS_FILTER_NO_UNIQUE_ADDRESS
Assertion used for programmer errors (misuse of the API).
Definition Macros.h:38
bool isFinite(T const &x)
Definition Math.h:305
constexpr Eigen::Index valueOf(S const &space, AxisIndex< Paths, N > const &... indices)
Definition Discrete.h:1112
CategoricalParams< Space, Scalar > predict(CategoricalParams< Space, Scalar > const &prior, MarkovChain< Space, Scalar > const &chain)
Definition Categorical.h:1049
std::expected< CategoricalParams< Space, Scalar >, ChainError > stationary(MarkovChain< Space, Scalar > const &chain)
Definition Categorical.h:1069
Eigen::Index drawSample(Categorical< Space, Scalar > const &distribution, G &rng)
Definition Categorical.h:1111
std::vector< Eigen::Index > drawSamples(Categorical< Space, Scalar > const &distribution, Eigen::Index n, G &rng)
Definition Categorical.h:1087
ChainError
Reasons a Markov chain has no single stationary distribution.
Definition Categorical.h:48
@ Reducible
some value can't be reached from some other, so the chain is not irreducible
Definition Categorical.h:49
Eigen::Array< typename Derived::Scalar, Eigen::Dynamic, 1 > logLikelihoods(M const &measurement, Space const &space, Eigen::MatrixBase< Derived > const &states)
Definition Likelihood.h:408
Sample< std::remove_cvref_t< G > > sample(G &&function)
Definition Policies.h:665