EmrysFilter 0.1.0
State estimation utilities in C++23 using Eigen
Loading...
Searching...
No Matches
Emission.h
Go to the documentation of this file.
1#pragma once
2
11
12#include <concepts>
13#include <expected>
14#include <type_traits>
15#include <utility>
16
27
28namespace Emrys
29{
30
36template <DiscreteSpace Space, typename Scalar>
37using EmissionBuffer = Eigen::Ref<Eigen::Array<Scalar, Space::Cardinality, 1>>;
38
49template <typename E, typename Space>
52 && requires(E const & emission, Space const & space, EmissionBuffer<Space, typename E::Scalar> out) {
54 emission.logLikelihoods(space, out);
55 };
56
64template <DiscreteSpace Space, RealScalar Scalar>
70
71namespace internal
72{
73
77template <typename Scalar, typename Space, EmissionModel<Space> E>
79emissionLogLikelihoods(CategoricalParams<Space, Scalar> const & state, E const & emission)
80{
82 auto logLikelihoods = Buffer(state.cardinality());
83 emission.logLikelihoods(state.space(), logLikelihoods);
84 return logLikelihoods;
85}
86
87} // namespace internal
88
95template <typename Scalar, typename Space, EmissionModel<Space> E>
96[[nodiscard]] std::expected<CategoricalUpdate<Space, Scalar>, WeightError>
97update(CategoricalParams<Space, Scalar> const & prior, E const & emission)
98{
99 auto result = CategoricalUpdate<Space, Scalar>{prior, Scalar(0)};
100 auto const evidence = result.posterior.update(internal::emissionLogLikelihoods(prior, emission));
101 if (!evidence)
102 {
103 return std::unexpected{evidence.error()};
104 }
105 result.logEvidence = *evidence;
106 return result;
107}
108
129template <DiscreteSpace Space_, DiscreteSpace ObservationSpace_, RealScalar Scalar_ = double>
131{
132public:
133
134 using Scalar = Scalar_;
135 using SpaceType = Space_;
136 using ObservationSpaceType = ObservationSpace_;
137
139 using MatrixType = Eigen::Matrix<
140 Scalar_,
141 Space_::Cardinality,
142 ObservationSpace_::Cardinality,
143 Eigen::RowMajor,
144 Space_::MaxCardinality,
145 ObservationSpace_::MaxCardinality>;
146
152 {
153 public:
154
155 using Scalar = Scalar_;
156
158 Observation(EmissionTable const & table, Eigen::Index observed)
159 : m_table{&table}
160 , m_observed{observed}
161 {
162 EMRYS_FILTER_ASSERT(
163 observed >= 0 && observed < table.m_matrix.cols() && "the observed value is out of range"
164 );
165 }
166
168 [[nodiscard]] Eigen::Index
169 observed() const
170 {
171 return m_observed;
172 }
173
175 void
177 {
178 EMRYS_FILTER_ASSERT(space == m_table->m_space && "the table is over a different space");
179 EMRYS_FILTER_ASSERT(out.size() == m_table->m_matrix.rows());
180 out = m_table->m_matrix.col(m_observed).array().log();
181 }
182
183 private:
184
185 EmissionTable const * m_table;
186 Eigen::Index m_observed;
187 };
188
192 template <MatrixExpression Matrix>
193 EmissionTable(Space_ space, ObservationSpace_ observations, Matrix && matrix)
194 : m_space{std::move(space)}
195 , m_observations{std::move(observations)}
196 , m_matrix{internal::castScalar<Scalar>(std::forward<Matrix>(matrix))}
197 {
198 EMRYS_FILTER_ASSERT(
199 m_matrix.rows() == m_space.cardinality() && m_matrix.cols() == m_observations.cardinality()
200 && "the emission matrix has a row per value and a column per observed value"
201 );
202 EMRYS_FILTER_ASSERT((m_matrix.array() >= Scalar(0)).all() && "emission probabilities must be nonnegative");
203 EMRYS_FILTER_ASSERT(
204 ((m_matrix.rowwise().sum().array() - Scalar(1)).abs() <= Eigen::NumTraits<Scalar>::dummy_precision()).all()
205 && "each row of the emission matrix must sum to one"
206 );
207 }
208
210 [[nodiscard]] Space_ const &
211 space() const
212 {
213 return m_space;
214 }
215
217 [[nodiscard]] ObservationSpace_ const &
219 {
220 return m_observations;
221 }
222
224 [[nodiscard]] MatrixType const &
225 matrix() const
226 {
227 return m_matrix;
228 }
229
231 [[nodiscard]] Observation
232 observe(Eigen::Index observed) const &
233 {
234 return Observation{*this, observed};
235 }
236
240 void
241 observe(Eigen::Index observed) const && = delete;
242
243private:
244
245 Space_ m_space;
246 ObservationSpace_ m_observations;
247 MatrixType m_matrix;
248};
249
251template <DiscreteSpace Space, DiscreteSpace ObservationSpace, MatrixExpression Matrix>
252EmissionTable(Space, ObservationSpace, Matrix &&)
254
256
274template <typename Observation, typename DistributionOf>
276{
277public:
278
280 using Scalar = std::remove_cvref_t<decltype(std::declval<DistributionOf const &>()(Eigen::Index{})
281 .logDensity(std::declval<Observation const &>()))>;
282
284 DensityEmission(Observation observation, DistributionOf distributionOf)
285 : m_observation{std::move(observation)}
286 , m_distributionOf{std::move(distributionOf)}
287 {}
288
290 [[nodiscard]] Observation const &
292 {
293 return m_observation;
294 }
295
297 template <DiscreteSpace Space>
298 void
300 {
301 EMRYS_FILTER_ASSERT(out.size() == space.cardinality());
302 for (Eigen::Index v = 0; v < out.size(); ++v)
303 {
304 out(v) = m_distributionOf(v).logDensity(m_observation);
305 }
306 }
307
308private:
309
310 Observation m_observation;
311 DistributionOf m_distributionOf;
312};
313
314namespace internal
315{
316
320template <DiscreteSpace Space, RealScalar Scalar>
321class ValueRates
322{
323public:
324
326 using ArrayType = typename CategoricalParams<Space, Scalar>::ArrayType;
327
329 template <typename Derived>
330 explicit ValueRates(Eigen::DenseBase<Derived> const & rates)
331 : m_rates{rates.derived().array().template cast<Scalar>()}
332 {
333 EMRYS_FILTER_ASSERT((m_rates >= Scalar(0)).all() && "rates must be nonnegative");
334 }
335
337 [[nodiscard]] ArrayType const &
338 rates() const
339 {
340 return m_rates;
341 }
342
343private:
344
345 ArrayType m_rates;
346};
347
348} // namespace internal
349
365template <DiscreteSpace Space, RealScalar Scalar_ = double>
366class PoissonEmission : public internal::ValueRates<Space, Scalar_>
367{
368public:
369
370 using Scalar = Scalar_;
371
376 template <typename Derived, std::integral Count>
377 requires(!std::same_as<Count, bool>)
378 PoissonEmission(Eigen::DenseBase<Derived> const & rates, Count count, TimeStep<Scalar_> auto const & dt)
379 : internal::ValueRates<Space, Scalar_>{rates}
380 , m_count{Eigen::Index(count)}
381 , m_dt{internal::toScalarTime<Scalar_>(dt)}
382 {
383 EMRYS_FILTER_ASSERT(count >= 0 && "the count must be nonnegative");
384 EMRYS_FILTER_ASSERT(m_dt > 0 && "the interval must be positive");
385 }
386
388 [[nodiscard]] Eigen::Index
389 count() const
390 {
391 return m_count;
392 }
393
395 [[nodiscard]] Scalar_
396 interval() const
397 {
398 return m_dt;
399 }
400
402 void
404 {
405 auto const & rates = this->rates();
406 EMRYS_FILTER_ASSERT(out.size() == space.cardinality() && rates.size() == out.size());
407 Scalar const n = Scalar(m_count);
408 Scalar const logFactorial = logGamma<Scalar>(n + Scalar(1));
409 for (Eigen::Index v = 0; v < out.size(); ++v)
410 {
411 Scalar const mean = rates(v) * m_dt;
412 if (mean == Scalar(0))
413 {
414 out(v) = m_count == 0 ? Scalar(0) : -Eigen::NumTraits<Scalar>::infinity();
415 }
416 else
417 {
418 out(v) = n * Eigen::numext::log(mean) - mean - logFactorial;
419 }
420 }
421 }
422
423private:
424
425 Eigen::Index m_count;
426 Scalar_ m_dt;
427};
428
441template <DiscreteSpace Space, RealScalar Scalar_ = double>
442class ExponentialEmission : public internal::ValueRates<Space, Scalar_>
443{
444public:
445
446 using Scalar = Scalar_;
447
451 template <typename Derived>
452 ExponentialEmission(Eigen::DenseBase<Derived> const & rates, TimeStep<Scalar_> auto const & tau)
453 : internal::ValueRates<Space, Scalar_>{rates}
454 , m_tau{internal::toScalarTime<Scalar_>(tau)}
455 {}
456
458 [[nodiscard]] Scalar_
459 interval() const
460 {
461 return m_tau;
462 }
463
465 void
467 {
468 auto const & rates = this->rates();
469 EMRYS_FILTER_ASSERT(out.size() == space.cardinality() && rates.size() == out.size());
470 for (Eigen::Index v = 0; v < out.size(); ++v)
471 {
472 out(v) = Eigen::numext::log(rates(v)) - rates(v) * m_tau;
473 }
474 }
475
476private:
477
478 Scalar_ m_tau;
479};
480
500template <DiscreteSpace Space, RealScalar Scalar_ = double>
501class ArrivalInterval : public internal::ValueRates<Space, Scalar_>
502{
503public:
504
505 using Scalar = Scalar_;
506
508 template <typename Derived>
509 explicit ArrivalInterval(Eigen::DenseBase<Derived> const & rates, bool endsInArrival = true)
510 : internal::ValueRates<Space, Scalar_>{rates}
511 , m_endsInArrival{endsInArrival}
512 {}
513
515 [[nodiscard]] bool
517 {
518 return m_endsInArrival;
519 }
520
521private:
522
523 bool m_endsInArrival;
524};
525
526namespace internal
527{
528
536template <typename Space, typename Scalar>
537struct ArrivalKernel
538{
540 Scalar logScale;
541};
542
547template <typename Space, typename Scalar>
548[[nodiscard]] ArrivalKernel<Space, Scalar>
549arrivalKernel(
550 ContinuousTimeMarkovChain<Space, Scalar> const & chain,
551 Scalar dt,
552 ArrivalInterval<Space, Scalar> const & interval
553)
554{
555 EMRYS_FILTER_ASSERT(dt >= 0 && "the interval must be nonnegative");
557 auto const & rates = interval.rates();
558 EMRYS_FILTER_ASSERT(rates.size() == chain.space().cardinality() && "give one rate per value");
559 Scalar const shift = rates.minCoeff();
560 MatrixType exponent = chain.generator() * dt;
561 exponent.diagonal().array() -= (rates - shift) * dt;
562 MatrixType kernel = exponent.exp();
563 kernel = kernel.cwiseMax(Scalar(0));
564 if (interval.endsInArrival())
565 {
566 kernel = kernel * rates.matrix().asDiagonal();
567 }
568 return {std::move(kernel), -shift * dt};
569}
570
575template <typename Space, typename Scalar, typename Kernel>
576[[nodiscard]] std::expected<CategoricalUpdate<Space, Scalar>, WeightError>
577moveByKernel(CategoricalParams<Space, Scalar> const & prior, Kernel const & kernel, Scalar logScale)
578{
579 using ArrayType = typename CategoricalParams<Space, Scalar>::ArrayType;
580 auto const priorTotal = prior.logTotalWeight();
581 if (!priorTotal)
582 {
583 return std::unexpected{priorTotal.error()};
584 }
585 ArrayType const logMoved = movedLogWeights<Scalar>(prior.logWeights(), kernel);
586 auto const total = logTotalWeight<Scalar>(logMoved);
587 if (!total)
588 {
589 return std::unexpected{total.error()};
590 }
591 return CategoricalUpdate<Space, Scalar>{
592 CategoricalParams<Space, Scalar>{prior.space(), logMoved - *total},
593 *total - *priorTotal + logScale
594 };
595}
596
597} // namespace internal
598
609template <typename Scalar, typename Space>
610[[nodiscard]] std::expected<CategoricalUpdate<Space, Scalar>, WeightError>
614 TimeStep<Scalar> auto const & dt,
615 ArrivalInterval<Space, Scalar> const & interval
616)
617{
618 EMRYS_FILTER_ASSERT(prior.space() == chain.space() && "the chain is on a different space");
619 auto const step = internal::arrivalKernel(chain, internal::toScalarTime<Scalar>(dt), interval);
620 return internal::moveByKernel(prior, step.kernel, step.logScale);
621}
622
623} // namespace Emrys
The distribution over a discrete space (decisions.md [discrete-spaces], [params-and-density]): Catego...
The continuous-time chain on a discrete space: ContinuousTimeMarkovChain, built from a generator of r...
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'...
The Space concept and what every continuous space shares (decisions.md [sizes-and-dimensions]): the t...
Time steps (decisions.md [time-steps]): TimeTraits, which converts a time type to a scalar,...
Log-weights: the WeightError a normalization can fail with (NumericError.h), and the streaming log-su...
The observation of a Markov-modulated Poisson process over an interval: no event inside it,...
Definition Emission.h:502
ArrivalInterval(Eigen::DenseBase< Derived > const &rates, bool endsInArrival=true)
The interval with rates one per value, ending in an event if endsInArrival.
Definition Emission.h:509
Scalar_ Scalar
the scalar type of the rates
Definition Emission.h:505
bool endsInArrival() const
Definition Emission.h:516
The parameters of a probability distribution over a discrete space: one weight per value.
Definition Categorical.h:257
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
Space_ const & space() const
Definition Categorical.h:330
Eigen::Array< Scalar_, CardinalityAtCompileTime, 1, Eigen::ColMajor, MaxCardinalityAtCompileTime, 1 > ArrayType
One entry per value, in flat-index order.
Definition Categorical.h:268
A continuous-time Markov chain on a discrete space: the rate of moving from each value to each other ...
Definition ContinuousTimeMarkovChain.h:61
Space_ const & space() const
Definition ContinuousTimeMarkovChain.h:227
typename MarkovChain< Space_, Scalar_ >::MatrixType MatrixType
The generator's type, the same as a MarkovChain's transition matrix.
Definition ContinuousTimeMarkovChain.h:70
void logLikelihoods(Space const &space, EmissionBuffer< Space, Scalar > out) const
Writes the observation's log-density under each value's distribution into out.
Definition Emission.h:299
DensityEmission(Observation observation, DistributionOf distributionOf)
The emission of observation, under the distribution distributionOf returns for each value.
Definition Emission.h:284
Observation const & observation() const
Definition Emission.h:291
std::remove_cvref_t< decltype(std::declval< DistributionOf const & >()(Eigen::Index{}) .logDensity(std::declval< Observation const & >()))> Scalar
The scalar type of the log-densities.
Definition Emission.h:280
The emission model of one observed value: the column of the table it selects.
Definition Emission.h:152
Eigen::Index observed() const
Definition Emission.h:169
Scalar_ Scalar
the scalar type of the probabilities
Definition Emission.h:155
Observation(EmissionTable const &table, Eigen::Index observed)
The observation of the value with flat index observed under table.
Definition Emission.h:158
void logLikelihoods(Space_ const &space, EmissionBuffer< Space_, Scalar_ > out) const
Writes into out for each value of space.
Definition Emission.h:176
An emission table: the probability of each observed value given each value of the state.
Definition Emission.h:131
ObservationSpace_ ObservationSpaceType
the discrete space of the observation
Definition Emission.h:136
Observation observe(Eigen::Index observed) const &
Definition Emission.h:232
void observe(Eigen::Index observed) const &&=delete
observe() of a temporary table is deleted: the observation refers to the table, which would be gone a...
Eigen::Matrix< Scalar_, Space_::Cardinality, ObservationSpace_::Cardinality, Eigen::RowMajor, Space_::MaxCardinality, ObservationSpace_::MaxCardinality > MatrixType
The emission matrix's type: a row per value of the state, a column per observed value.
Definition Emission.h:139
ObservationSpace_ const & observationSpace() const
Definition Emission.h:218
MatrixType const & matrix() const
Definition Emission.h:225
Space_ SpaceType
the discrete space of the state
Definition Emission.h:135
Scalar_ Scalar
the scalar type of the probabilities
Definition Emission.h:134
EmissionTable(Space_ space, ObservationSpace_ observations, Matrix &&matrix)
The table over space and observations with emission matrix matrix: nonnegative, with each row summing...
Definition Emission.h:193
Space_ const & space() const
Definition Emission.h:211
void logLikelihoods(Space const &space, EmissionBuffer< Space, Scalar > out) const
Writes the exponential log-density of the inter-arrival time under each value's rate into out.
Definition Emission.h:466
ExponentialEmission(Eigen::DenseBase< Derived > const &rates, TimeStep< Scalar_ > auto const &tau)
The emission of an inter-arrival time of tau, any TimeStep (see Time steps), with rates one per value...
Definition Emission.h:452
Scalar_ interval() const
Definition Emission.h:459
Scalar_ Scalar
the scalar type of the rates
Definition Emission.h:446
Eigen::Index count() const
Definition Emission.h:389
Scalar_ interval() const
Definition Emission.h:396
Scalar_ Scalar
the scalar type of the rates
Definition Emission.h:370
PoissonEmission(Eigen::DenseBase< Derived > const &rates, Count count, TimeStep< Scalar_ > auto const &dt)
The emission of count events over an interval of dt, any TimeStep (see Time steps),...
Definition Emission.h:378
void logLikelihoods(Space const &space, EmissionBuffer< Space, Scalar > out) const
Writes the Poisson log-probability of the count under each value's rate into out.
Definition Emission.h:403
A type usable as a discrete space: a finite set of values.
Definition Discrete.h:72
A measurement of a value in the discrete space Space: one log-likelihood per value.
Definition Emission.h:50
A type usable as the scalar of a distribution: a real number type such as float, double,...
Definition Math.h:80
A type usable as the space of a distribution.
Definition Space.h:186
A step of time that converts to Scalar: Scalar itself, or any type with a TimeTraits,...
Definition Time.h:128
Scalar logGamma(std::type_identity_t< Scalar > const &x)
Definition Math.h:216
NumericError WeightError
Reasons a collection of log-weights cannot be normalized: Empty, ZeroSum, or NonFinite of NumericErro...
Definition NumericError.h:53
CategoricalParams< Space, Scalar > predict(CategoricalParams< Space, Scalar > const &prior, MarkovChain< Space, Scalar > const &chain)
Definition Categorical.h:1049
Eigen::Ref< Eigen::Array< Scalar, Space::Cardinality, 1 > > EmissionBuffer
The buffer an emission model writes its log-likelihoods into: one coefficient per value of the discre...
Definition Emission.h:37
std::expected< CategoricalUpdate< Space, Scalar >, WeightError > update(CategoricalParams< Space, Scalar > const &prior, E const &emission)
Definition Emission.h:97
Eigen::Array< typename Derived::Scalar, Eigen::Dynamic, 1 > logLikelihoods(M const &measurement, Space const &space, Eigen::MatrixBase< Derived > const &states)
Definition Likelihood.h:408
The result of updating a CategoricalParams by an emission model: the posterior, whose weights sum to ...
Definition Emission.h:66
Scalar logEvidence
Definition Emission.h:68
CategoricalParams< Space, Scalar > posterior
the distribution given the measurement
Definition Emission.h:67