aGrUM 3.2.0
a C++ library for (probabilistic) graphical models
KTBNInference_tpl.h
Go to the documentation of this file.
1/****************************************************************************
2 * This file is part of the aGrUM/pyAgrum library. *
3 * *
4 * Copyright (c) 2005-2026 by *
5 * - Pierre-Henri WUILLEMIN(_at_LIP6) *
6 * - Christophe GONZALES(_at_AMU) *
7 * *
8 * The aGrUM/pyAgrum library is free software; you can redistribute it *
9 * and/or modify it under the terms of either : *
10 * *
11 * - the GNU Lesser General Public License as published by *
12 * the Free Software Foundation, either version 3 of the License, *
13 * or (at your option) any later version, *
14 * - the MIT license (MIT), *
15 * - or both in dual license, as here. *
16 * *
17 * (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) *
18 * *
19 * This aGrUM/pyAgrum library is distributed in the hope that it will be *
20 * useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
21 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS *
22 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
25 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
26 * OTHER DEALINGS IN THE SOFTWARE. *
27 * *
28 * See LICENCES for more details. *
29 * *
30 * SPDX-FileCopyrightText: Copyright 2005-2026 *
31 * - Pierre-Henri WUILLEMIN(_at_LIP6) *
32 * - Christophe GONZALES(_at_AMU) *
33 * SPDX-License-Identifier: LGPL-3.0-or-later OR MIT *
34 * *
35 * Contact : info_at_agrum_dot_org *
36 * homepage : http://agrum.gitlab.io *
37 * gitlab : https://gitlab.com/agrumery/agrum *
38 * *
39 ****************************************************************************/
40
41
48
49#pragma once
50
51#include <algorithm>
52#include <cctype>
53#include <cmath>
54#include <limits>
55#include <sstream>
56
58
59namespace gum {
60
61 namespace {
64 constexpr NodeId KTBN_SKIP_NONE = std::numeric_limits< NodeId >::max();
65 constexpr NodeId KTBN_SKIP_PREV = std::numeric_limits< NodeId >::max() - 1;
66 constexpr NodeId KTBN_SKIP_NEXT = std::numeric_limits< NodeId >::max() - 2;
67 } // namespace
68
69 // ===========================================================================
70 // Constructor
71 // ===========================================================================
72
73 template < GUM_Numeric GUM_SCALAR >
75 if (ktbn == nullptr) GUM_ERROR(InvalidArgument, "KTBNInference: the k-DBN is null.")
76 _k_ = static_cast< int >(ktbn->k());
77
78 // a deterministic base order (the KTBN's own sets are unordered): temporal
79 // first, then atemporal, so a slot's base index alone says which it is.
80 _temporalSorted_.assign(ktbn->temporalVarNames().begin(), ktbn->temporalVarNames().end());
81 std::sort(_temporalSorted_.begin(), _temporalSorted_.end());
82 _atemporalSorted_.assign(ktbn->atemporalVarNames().begin(), ktbn->atemporalVarNames().end());
83 std::sort(_atemporalSorted_.begin(), _atemporalSorted_.end());
84
87 _baseNames_.insert(_baseNames_.end(), _atemporalSorted_.begin(), _atemporalSorted_.end());
88 for (std::size_t i = 0; i < _baseNames_.size(); ++i)
89 _baseIdx_[_baseNames_[i]] = static_cast< int >(i);
90
91 // compile windows for the default (all-bases) query; _markRequisite_()
92 // recompiles only if a later target/observation set changes the subset
93 _requisite_.assign(_baseNames_.size(), true);
95 }
96
97 // ===========================================================================
98 // Name encoding / decoding / validation
99 // ===========================================================================
100
101 template < GUM_Numeric GUM_SCALAR >
102 std::string KTBNInference< GUM_SCALAR >::_encode_(const std::string& base, int slice) {
103 if (slice == ATEMPORAL) return base;
104 return base + "[" + std::to_string(slice) + "]";
105 }
106
107 template < GUM_Numeric GUM_SCALAR >
108 std::pair< std::string, int >
109 KTBNInference< GUM_SCALAR >::_determineNode_(const std::string& name) const {
110 // a name registered as atemporal is atemporal, even if bracket-shaped
111 if (_ktbn_->atemporalVarNames().find(name) != _ktbn_->atemporalVarNames().end())
112 return {name, ATEMPORAL};
113
114 // only a well-formed "base[digits]" suffix is temporal (mirrors
115 // KTBN::_decodeName_); anything else falls through to atemporal
116 const auto lb = name.rfind('[');
117 if (lb == std::string::npos || name.back() != ']') return {name, ATEMPORAL};
118
119 const std::string inner = name.substr(lb + 1, name.size() - lb - 2);
120 if (inner.empty()) return {name, ATEMPORAL};
121 for (const char c: inner)
122 if (std::isdigit(static_cast< unsigned char >(c)) == 0) return {name, ATEMPORAL};
123
124 int slice{};
125 try {
126 slice = std::stoi(inner);
127 } catch (const std::out_of_range&) {
129 "Node name '" << name << "' has a slice index too large to represent as int.")
130 }
131 return {name.substr(0, lb), slice};
132 }
133
134 template < GUM_Numeric GUM_SCALAR >
135 void KTBNInference< GUM_SCALAR >::_validateNode_(const std::string& base, int slice) const {
136 const bool temporal = _isTemporal_(base);
137 const bool atemporal = _isAtemporal_(base);
138
139 if (!temporal && !atemporal) GUM_ERROR(NotFound, "Unknown variable '" << base << "'.")
140 if (temporal && slice < 0)
141 GUM_ERROR(InvalidArgument, "Temporal variable '" << base << "' requires a slice >= 0.")
142 if (atemporal && slice != ATEMPORAL)
143 GUM_ERROR(InvalidArgument, "Atemporal variable '" << base << "' has no time slice.")
144 }
145
146 template < GUM_Numeric GUM_SCALAR >
148 int slice) const {
149 if (slice == ATEMPORAL) return _ktbn_->variable(base, ATEMPORAL);
150 return _ktbn_->variable(base, (slice < _k_) ? slice : _k_ - 1);
151 }
152
153 template < GUM_Numeric GUM_SCALAR >
155 if (s.lag == ATEMPORAL) return &_ktbn_->variable(_baseNames_[s.base], ATEMPORAL);
156 return &_ktbn_->variable(_baseNames_[s.base], (t - s.lag) % _k_);
157 }
158
159 // ===========================================================================
160 // Interventions
161 // ===========================================================================
162
163 template < GUM_Numeric GUM_SCALAR >
165 int slice,
166 const KTBNModality& value) {
167 const std::string b(base);
168 _validateNode_(b, slice);
169 _interventions_[_encode_(b, slice)] = value.toIndex(_templateVar_(b, slice));
170 _done_ = false;
171 }
172
173 template < GUM_Numeric GUM_SCALAR >
174 void KTBNInference< GUM_SCALAR >::addIntervention(std::string_view node_name,
175 const KTBNModality& value) {
176 const auto [b, s] = _determineNode_(std::string(node_name));
177 addIntervention(b, s, value);
178 }
179
180 template < GUM_Numeric GUM_SCALAR >
182 const std::vector< std::pair< NodeKey, KTBNModality > >& interventions) {
183 // resolve and validate the whole batch first, so a bad entry cannot leave
184 // the engine with the preceding entries already applied
185 std::vector< std::pair< std::string, Idx > > resolved;
186 resolved.reserve(interventions.size());
187 for (const auto& [key, value]: interventions) {
188 const auto [b, s] = std::holds_alternative< std::string >(key)
189 ? _determineNode_(std::get< std::string >(key))
190 : std::get< std::pair< std::string, int > >(key);
191 _validateNode_(b, s);
192 resolved.emplace_back(_encode_(b, s), value.toIndex(_templateVar_(b, s)));
193 }
194
195 for (const auto& [name, idx]: resolved)
196 _interventions_[name] = idx;
197 if (!resolved.empty()) _done_ = false;
198 }
199
200 template < GUM_Numeric GUM_SCALAR >
201 void KTBNInference< GUM_SCALAR >::eraseIntervention(std::string_view base, int slice) {
202 _interventions_.erase(_encode_(std::string(base), slice));
203 _done_ = false;
204 }
205
206 template < GUM_Numeric GUM_SCALAR >
207 void KTBNInference< GUM_SCALAR >::eraseIntervention(std::string_view node_name) {
208 const auto [b, s] = _determineNode_(std::string(node_name));
209 eraseIntervention(b, s);
210 }
211
212 template < GUM_Numeric GUM_SCALAR >
217
218 template < GUM_Numeric GUM_SCALAR >
219 bool KTBNInference< GUM_SCALAR >::hasIntervention(std::string_view base, int slice) const {
220 return _interventions_.find(_encode_(std::string(base), slice)) != _interventions_.end();
221 }
222
223 template < GUM_Numeric GUM_SCALAR >
224 bool KTBNInference< GUM_SCALAR >::hasIntervention(std::string_view node_name) const {
225 const auto [b, s] = _determineNode_(std::string(node_name));
226 return hasIntervention(b, s);
227 }
228
229 // ===========================================================================
230 // Observations
231 // ===========================================================================
232
233 template < GUM_Numeric GUM_SCALAR >
235 int slice,
236 const KTBNModality& value) {
237 const std::string b(base);
238 _validateNode_(b, slice);
239 const DiscreteVariable& v = _templateVar_(b, slice);
240 const Idx idx = value.toIndex(v);
241 std::vector< GUM_SCALAR > like(v.domainSize(), GUM_SCALAR(0));
242 like[idx] = GUM_SCALAR(1);
243 _observations_[_encode_(b, slice)] = std::move(like);
244 _done_ = false;
245 }
246
247 template < GUM_Numeric GUM_SCALAR >
248 void KTBNInference< GUM_SCALAR >::addObservation(std::string_view node_name,
249 const KTBNModality& value) {
250 const auto [b, s] = _determineNode_(std::string(node_name));
251 addObservation(b, s, value);
252 }
253
254 template < GUM_Numeric GUM_SCALAR >
256 int slice,
257 const std::vector< GUM_SCALAR >& likelihood) {
258 const std::string b(base);
259 _validateNode_(b, slice);
260 const DiscreteVariable& v = _templateVar_(b, slice);
261 if (likelihood.size() != v.domainSize())
263 "Soft observation on '" << _encode_(b, slice) << "' needs " << v.domainSize()
264 << " values, got " << likelihood.size() << ".")
265 GUM_SCALAR total = GUM_SCALAR(0);
266 for (const auto x: likelihood) {
267 if (x < GUM_SCALAR(0))
269 "Soft observation on '" << _encode_(b, slice) << "' has a negative entry.")
270 total += x;
271 }
272 if (total <= GUM_SCALAR(0))
274 "Soft observation on '" << _encode_(b, slice) << "' is all zeros: impossible.")
275 _observations_[_encode_(b, slice)] = likelihood;
276 _done_ = false;
277 }
278
279 template < GUM_Numeric GUM_SCALAR >
280 void KTBNInference< GUM_SCALAR >::addObservation(std::string_view node_name,
281 const std::vector< GUM_SCALAR >& likelihood) {
282 const auto [b, s] = _determineNode_(std::string(node_name));
283 addObservation(b, s, likelihood);
284 }
285
286 template < GUM_Numeric GUM_SCALAR >
288 const std::vector< std::pair< NodeKey, KTBNModality > >& observations) {
289 // all-or-nothing, like the batch addIntervention()
290 std::vector< std::pair< std::string, std::vector< GUM_SCALAR > > > resolved;
291 resolved.reserve(observations.size());
292 for (const auto& [key, value]: observations) {
293 const auto [b, s] = std::holds_alternative< std::string >(key)
294 ? _determineNode_(std::get< std::string >(key))
295 : std::get< std::pair< std::string, int > >(key);
296 _validateNode_(b, s);
297 const DiscreteVariable& v = _templateVar_(b, s);
298 std::vector< GUM_SCALAR > like(v.domainSize(), GUM_SCALAR(0));
299 like[value.toIndex(v)] = GUM_SCALAR(1);
300 resolved.emplace_back(_encode_(b, s), std::move(like));
301 }
302 for (auto& [name, like]: resolved)
303 _observations_[name] = std::move(like);
304 if (!resolved.empty()) _done_ = false;
305 }
306
307 template < GUM_Numeric GUM_SCALAR >
308 void KTBNInference< GUM_SCALAR >::eraseObservation(std::string_view base, int slice) {
309 _observations_.erase(_encode_(std::string(base), slice));
310 _done_ = false;
311 }
312
313 template < GUM_Numeric GUM_SCALAR >
314 void KTBNInference< GUM_SCALAR >::eraseObservation(std::string_view node_name) {
315 const auto [b, s] = _determineNode_(std::string(node_name));
316 eraseObservation(b, s);
317 }
318
319 template < GUM_Numeric GUM_SCALAR >
324
325 template < GUM_Numeric GUM_SCALAR >
326 bool KTBNInference< GUM_SCALAR >::hasObservation(std::string_view base, int slice) const {
327 return _observations_.find(_encode_(std::string(base), slice)) != _observations_.end();
328 }
329
330 template < GUM_Numeric GUM_SCALAR >
331 bool KTBNInference< GUM_SCALAR >::hasObservation(std::string_view node_name) const {
332 const auto [b, s] = _determineNode_(std::string(node_name));
333 return hasObservation(b, s);
334 }
335
336 template < GUM_Numeric GUM_SCALAR >
338 return !_observations_.empty();
339 }
340
341 // ===========================================================================
342 // Targets
343 // ===========================================================================
344
345 template < GUM_Numeric GUM_SCALAR >
346 void KTBNInference< GUM_SCALAR >::addTarget(std::string_view base) {
347 const std::string b(base);
348 if (!_isTemporal_(b) && !_isAtemporal_(b))
349 GUM_ERROR(NotFound, "Unknown variable '" << b << "'.")
350 _targets_.insert(b);
351 _targeted_mode_ = true;
352 _done_ = false;
353 }
354
355 template < GUM_Numeric GUM_SCALAR >
356 void KTBNInference< GUM_SCALAR >::eraseTarget(std::string_view base) {
357 _targets_.erase(std::string(base));
358 if (_targets_.empty()) _targeted_mode_ = false;
359 _done_ = false;
360 }
361
362 template < GUM_Numeric GUM_SCALAR >
364 _targets_.clear();
365 _targeted_mode_ = false;
366 _done_ = false;
367 }
368
369 template < GUM_Numeric GUM_SCALAR >
370 bool KTBNInference< GUM_SCALAR >::isTarget(std::string_view base) const {
371 const std::string b(base);
372 if (!_targeted_mode_) return _isTemporal_(b) || _isAtemporal_(b);
373 return _targets_.contains(b);
374 }
375
376 template < GUM_Numeric GUM_SCALAR >
380
381 // ===========================================================================
382 // Structure: interfaces and window compilation
383 // ===========================================================================
384
385 template < GUM_Numeric GUM_SCALAR >
386 std::vector< typename KTBNInference< GUM_SCALAR >::_Slot_ >
388 std::vector< _Slot_ > out;
389 const std::string& b = _baseNames_[baseIdx];
390
391 if (baseIdx >= static_cast< int >(_nbTemporal_)) {
392 out.push_back({baseIdx, ATEMPORAL});
393 for (const auto& [pb, ps]: _ktbn_->parents(b, ATEMPORAL)) {
394 (void)ps;
395 out.push_back({_baseIdx_.at(pb), ATEMPORAL});
396 }
397 return out;
398 }
399
400 // initial CPTs while inside the initial block, else the (time-invariant)
401 // transition kernel -- why one window template serves every later t
402 const int structSlice = (t <= _k_ - 1) ? t : _k_ - 1;
403 out.push_back({baseIdx, 0});
404 for (const auto& [pb, ps]: _ktbn_->parents(b, structSlice)) {
405 if (ps == ATEMPORAL) out.push_back({_baseIdx_.at(pb), ATEMPORAL});
406 else out.push_back({_baseIdx_.at(pb), structSlice - ps});
407 }
408 return out;
409 }
410
411 template < GUM_Numeric GUM_SCALAR >
413 int last = -1;
414
415 // consumers inside the initial block (their parent sets are per-slice)
416 for (int tc = 0; tc <= _k_ - 2; ++tc) {
417 if (tc <= last) continue;
418 for (int c = 0; c < static_cast< int >(_nbTemporal_); ++c) {
419 if (!_requisite_[c]) continue;
420 for (const auto& [pb, ps]: _ktbn_->parents(_baseNames_[c], tc)) {
421 if (ps == ATEMPORAL) continue;
422 if (ps == s && _baseIdx_.at(pb) == baseIdx) {
423 last = tc;
424 break;
425 }
426 }
427 if (tc == last) break;
428 }
429 }
430
431 // repeating block: the kernel reaches back _maxLag_ slices, so the last
432 // consumer sits at s + maxLag
433 if (_maxLag_[baseIdx] >= 1) {
434 const int tc = s + _maxLag_[baseIdx];
435 if (tc >= _k_ - 1 && tc > last) last = tc;
436 }
437 return last;
438 }
439
440 template < GUM_Numeric GUM_SCALAR >
441 std::vector< typename KTBNInference< GUM_SCALAR >::_Slot_ >
443 std::vector< _Slot_ > out;
444 for (int i = 0; i < static_cast< int >(_nbTemporal_); ++i) {
445 if (!_requisite_[i]) continue;
446 for (int s = t; s >= 0 && t - s <= _k_ - 1; --s)
447 if (_lastConsumerSlice_(i, s) > t) out.push_back({i, t - s});
448 }
449 // an atemporal variable is a parent at every slice, so it never leaves
450 // the interface -- the one thing that couples the whole horizon
451 for (int i = static_cast< int >(_nbTemporal_); i < static_cast< int >(_baseNames_.size()); ++i)
452 if (_requisite_[i]) out.push_back({i, ATEMPORAL});
453 return out;
454 }
455
456 template < GUM_Numeric GUM_SCALAR >
458 KTBNInference< GUM_SCALAR >::_compileWindow_(const std::vector< _Slot_ >& Iprev,
459 const std::vector< _Slot_ >& Icur,
460 int t,
461 bool withAtemporalFamilies) const {
462 _Window_ w;
463 w.Iprev = Iprev;
464 w.Icur = Icur;
465
466 // ---- node set: H_t = I_{t-1} U V_t (plus the atemporal layer at t == 0) --
467 std::map< std::pair< int, int >, NodeId > id;
469 NodeProperty< Size > domSizes;
470
471 const auto ensure = [&](const _Slot_& s) -> NodeId {
472 const auto key = std::make_pair(s.base, s.lag);
473 const auto it = id.find(key);
474 if (it != id.end()) return it->second;
475 const NodeId nid = graph.addNode();
476 id[key] = nid;
477 if (static_cast< std::size_t >(nid) >= w.slotOfNode.size()) w.slotOfNode.resize(nid + 1);
478 w.slotOfNode[nid] = s;
479 domSizes.insert(nid,
480 (s.lag == ATEMPORAL)
481 ? _ktbn_->variable(_baseNames_[s.base], ATEMPORAL).domainSize()
482 : _ktbn_->variable(_baseNames_[s.base], _k_ - 1).domainSize());
483 return nid;
484 };
485
486 for (const auto& s: Iprev)
487 ensure(s);
488 for (int i = 0; i < static_cast< int >(_nbTemporal_); ++i)
489 if (_requisite_[i]) ensure({i, 0});
490 if (withAtemporalFamilies)
491 for (int i = static_cast< int >(_nbTemporal_); i < static_cast< int >(_baseNames_.size());
492 ++i)
493 if (_requisite_[i]) ensure({i, ATEMPORAL});
494 for (const auto& s: Icur)
495 ensure(s);
496
497 if (graph.size() == 0) return w; // nothing requisite: an empty window
498
499 // ---- moralise: every family becomes a clique ----------------------------
500 std::vector< std::pair< int, std::vector< NodeId > > > families;
501 const auto addFamily = [&](int b) {
502 std::vector< NodeId > scope;
503 for (const auto& s: _familySlots_(b, t))
504 scope.push_back(ensure(s));
505 for (std::size_t a = 0; a < scope.size(); ++a)
506 for (std::size_t c = a + 1; c < scope.size(); ++c)
507 if (!graph.existsEdge(scope[a], scope[c])) graph.addEdge(scope[a], scope[c]);
508 families.emplace_back(b, std::move(scope));
509 };
510 for (int i = 0; i < static_cast< int >(_nbTemporal_); ++i)
511 if (_requisite_[i]) addFamily(i);
512 if (withAtemporalFamilies)
513 for (int i = static_cast< int >(_nbTemporal_); i < static_cast< int >(_baseNames_.size());
514 ++i)
515 if (_requisite_[i]) addFamily(i);
516
517 // ---- force each interface to be a clique --------------------------------
518 // Murphy's constraint: one clique must hold the whole interface, so the
519 // message crossing the slice boundary is a single potential.
520 const auto makeClique = [&](const std::vector< _Slot_ >& slots) {
521 std::vector< NodeId > ids;
522 for (const auto& s: slots)
523 ids.push_back(ensure(s));
524 for (std::size_t a = 0; a < ids.size(); ++a)
525 for (std::size_t c = a + 1; c < ids.size(); ++c)
526 if (!graph.existsEdge(ids[a], ids[c])) graph.addEdge(ids[a], ids[c]);
527 };
528 makeClique(Iprev);
529 makeClique(Icur);
530
531 // ---- triangulate once; this tree is reused for every slice and horizon --
532 DefaultTriangulation tri(&graph, &domSizes);
533 w.jt = tri.junctionTree();
534
535 const auto covers = [&](NodeId c, const std::vector< _Slot_ >& slots) {
536 const NodeSet& cl = w.jt.clique(c);
537 for (const auto& s: slots)
538 if (!cl.contains(id.at({s.base, s.lag}))) return false;
539 return true;
540 };
541
542 bool foundC = false, foundD = false;
543 for (const NodeId c: w.jt.nodes()) {
544 if (!foundC && covers(c, Icur)) {
545 w.rootC = c;
546 foundC = true;
547 }
548 if (!foundD && covers(c, Iprev)) {
549 w.rootD = c;
550 foundD = true;
551 }
552 }
553 if (!foundC || !foundD)
554 GUM_ERROR(FatalError, "KTBNInference: the window interfaces did not end up in a clique.")
555
556 // ---- assign every family factor to one clique that covers it ------------
557 for (const auto& [b, scope]: families) {
558 bool placed = false;
559 for (const NodeId c: w.jt.nodes()) {
560 const NodeSet& cl = w.jt.clique(c);
561 bool ok = true;
562 for (const NodeId n: scope)
563 if (!cl.contains(n)) {
564 ok = false;
565 break;
566 }
567 if (ok) {
568 w.factorsOf[c].push_back(b);
569 placed = true;
570 break;
571 }
572 }
573 if (!placed)
574 GUM_ERROR(FatalError, "KTBNInference: family of '" << _baseNames_[b] << "' fits no clique.")
575 }
576
577 // ---- where to read each base's own marginal (and place its observation) ----
578 for (const auto& [key, nid]: id) {
579 const int base = key.first;
580 for (const NodeId c: w.jt.nodes())
581 if (w.jt.clique(c).contains(nid)) {
582 if (key.second == 0 || key.second == ATEMPORAL) w.selfClique[base] = c;
583 break;
584 }
585 }
586
587 // ---- stitch a junction FOREST into a junction tree ----------------------
588 // Barren processes and unused atemporal variables sit in their own
589 // components. Linking each to C_t with an EMPTY separator makes one tree
590 // without touching any marginal -- the crossing message is just a scalar.
591 {
592 std::unordered_map< NodeId, int > comp;
593 int nc = 0;
594 for (const NodeId s: w.jt.nodes()) {
595 if (comp.count(s) != 0) continue;
596 std::vector< NodeId > q{s};
597 comp[s] = nc;
598 for (std::size_t i = 0; i < q.size(); ++i)
599 for (const NodeId nb: w.jt.neighbours(q[i]))
600 if (comp.count(nb) == 0) {
601 comp[nb] = nc;
602 q.push_back(nb);
603 }
604 ++nc;
605 }
606 if (nc > 1) {
607 std::unordered_set< int > linked{comp.at(w.rootC)};
608 for (const NodeId n: w.jt.nodes())
609 if (linked.insert(comp.at(n)).second) w.jt.addEdge(w.rootC, n);
610 }
611 }
612
613 // ---- root the tree at C_t and record a BFS order ------------------------
614 std::unordered_set< NodeId > seen;
615 w.bfs.push_back(w.rootC);
616 seen.insert(w.rootC);
617 w.parentOf[w.rootC] = w.rootC;
618 for (std::size_t qi = 0; qi < w.bfs.size(); ++qi) {
619 const NodeId cur = w.bfs[qi];
620 for (const NodeId nb: w.jt.neighbours(cur))
621 if (seen.insert(nb).second) {
622 w.parentOf[nb] = cur;
623 w.bfs.push_back(nb);
624 }
625 }
626 // the forced interface cliques rule out a disconnected forest; assert it anyway
627 if (w.bfs.size() != w.jt.size())
628 GUM_ERROR(FatalError, "KTBNInference: the window junction tree is not connected.")
629
630 return w;
631 }
632
633 template < GUM_Numeric GUM_SCALAR >
635 // how long an occurrence must survive: the deepest lag the requisite
636 // kernel still reads it at -- what bounds |I|
637 _maxLag_.assign(_nbTemporal_, 0);
638 for (int c = 0; c < static_cast< int >(_nbTemporal_); ++c) {
639 if (!_requisite_[c]) continue;
640 for (const auto& [pb, ps]: _ktbn_->parents(_baseNames_[c], _k_ - 1)) {
641 if (ps == ATEMPORAL) continue;
642 const int i = _baseIdx_.at(pb);
643 const int lag = (_k_ - 1) - ps;
644 if (lag >= 1 && lag > _maxLag_[i]) _maxLag_[i] = lag;
645 }
646 }
647
648 const auto shift = [](std::vector< _Slot_ > v) {
649 for (auto& s: v)
650 if (s.lag != ATEMPORAL) ++s.lag;
651 return v;
652 };
653
654 // windows 0..k-1 cover slices 0..k-1; window k is the repeating one, used
655 // from slice k on. For k>=2, k-1 and k are structurally identical, so the
656 // extra compile is free; for k==1 they must differ, since slice 0 alone
657 // carries the atemporal layer in.
658 _windows_.clear();
659 _windows_.reserve(_k_ + 1);
660 std::vector< _Slot_ > prev; // I_{t-1}, in slice-t coordinates
661 for (int t = 0; t <= _k_; ++t) {
662 std::vector< _Slot_ > cur = _interfaceAfter_(t);
663 _windows_.push_back(_compileWindow_(prev, cur, t, /*withAtemporalFamilies=*/t == 0));
664 prev = shift(std::move(cur));
665 }
666 }
667
668 template < GUM_Numeric GUM_SCALAR >
670 std::vector< bool > req(_baseNames_.size(), false);
671 std::vector< int > stack;
672
673 const auto push = [&](const std::string& b) {
674 const auto it = _baseIdx_.find(b);
675 if (it == _baseIdx_.end()) return;
676 if (!req[it->second]) {
677 req[it->second] = true;
678 stack.push_back(it->second);
679 }
680 };
681
682 if (!_targeted_mode_) {
683 for (const auto& b: _baseNames_)
684 push(b);
685 } else {
686 for (const auto& b: _targets_)
687 push(b);
688 }
689 // an observed node is requisite even if barren: its likelihood is what
690 // revises everything upstream of it.
691 for (const auto& [name, like]: _observations_) {
692 (void)like;
693 push(_determineNode_(name).first);
694 }
695
696 while (!stack.empty()) {
697 const int i = stack.back();
698 stack.pop_back();
699 const std::string& b = _baseNames_[i];
700 if (i < static_cast< int >(_nbTemporal_)) {
701 for (int t = 0; t <= _k_ - 1; ++t)
702 for (const auto& [pb, ps]: _ktbn_->parents(b, t)) {
703 (void)ps;
704 push(pb);
705 }
706 } else {
707 for (const auto& [pb, ps]: _ktbn_->parents(b, ATEMPORAL)) {
708 (void)ps;
709 push(pb);
710 }
711 }
712 }
713
714 if (req != _requisite_) {
715 _requisite_ = std::move(req);
717 }
718 }
719
720 // ===========================================================================
721 // Runtime: filling and message passing
722 // ===========================================================================
723
724 template < GUM_Numeric GUM_SCALAR >
725 const Tensor< GUM_SCALAR >& KTBNInference< GUM_SCALAR >::_buildKernel_(const std::string& p,
726 int t) const {
727 // The result depends on t only through t % k -- for the child directly, and
728 // for each parent as (t - lag) % k, which is fixed once t % k is. So one
729 // tensor per (process, phase) serves every slice of that phase.
730 const std::pair< std::string, int > key{p, static_cast< int >(t % _k_)};
731 const auto hit = _kernelCache_.find(key);
732 if (hit != _kernelCache_.end()) return hit->second;
733
734 // Builds P(p[t] | its absolute-time parents) by rolling the template
735 // kernel P(p[k-1] | parents) forward onto slice t. The kernel's own axes
736 // use ABSOLUTE-slice identity (variable(base, slice % k), the KTBN's
737 // cycled per-process objects); srcNames uses the TEMPLATE's time-invariant
738 // names, only to pick which axis of cpt(p, k-1) to copy from -- fillWith's
739 // mapSrc maps by position, not name, so the two need not match.
740 Tensor< GUM_SCALAR > kernel;
741 std::vector< std::string > srcNames;
742
743 kernel << _ktbn_->variable(p, t % _k_);
744 srcNames.push_back(_encode_(p, _k_ - 1));
745
746 for (const auto& [parBase, parSlice]: _ktbn_->parents(p, _k_ - 1)) {
747 if (parSlice == ATEMPORAL) {
748 kernel << _ktbn_->variable(parBase, ATEMPORAL);
749 srcNames.push_back(parBase);
750 } else {
751 const int lag = (_k_ - 1) - parSlice;
752 kernel << _ktbn_->variable(parBase, (t - lag) % _k_);
753 srcNames.push_back(_encode_(parBase, parSlice));
754 }
755 }
756 kernel.fillWith(_ktbn_->cpt(p, _k_ - 1), srcNames);
757 return _kernelCache_.emplace(key, std::move(kernel)).first->second;
758 }
759
760 template < GUM_Numeric GUM_SCALAR >
762 const _Window_& w,
763 int t,
764 std::unordered_map< NodeId, Tensor< GUM_SCALAR > >& psi) const {
765 for (const auto& [c, bases]: w.factorsOf)
766 for (const int b: bases) {
767 if (b >= static_cast< int >(_nbTemporal_)) continue; // atemporal: already in the base
768 const auto ite = _observations_.find(_encode_(_baseNames_[b], t));
769 if (ite == _observations_.end()) continue;
770 Tensor< GUM_SCALAR > ev;
771 ev << _ktbn_->variable(_baseNames_[b], t % _k_);
772 ev.fillWith(ite->second);
773 psi.at(c) *= ev;
774 }
775 }
776
777 template < GUM_Numeric GUM_SCALAR >
779 // t < k: the initial windows, each its own. t >= k: the repeating window,
780 // one slot per phase. Keying on t % k alone would collide slice 2 with
781 // slice 5 at k=3 -- same phase, different window and different CPTs.
782 return (t < static_cast< int >(_k_)) ? static_cast< Size >(t)
783 : _k_ + static_cast< Size >(t % static_cast< int >(_k_));
784 }
785
786 template < GUM_Numeric GUM_SCALAR >
787 const std::unordered_map< NodeId, Tensor< GUM_SCALAR > >&
789 // do(X=x) replaces a CPT the base already applied, so the base is unusable
790 if (_interventionSlices_.contains(t)) {
791 _fillWindow_(w, t, _psiScratch_, true);
792 return _psiScratch_;
793 }
794
795 const Size key = _psiKey_(t);
796 if (!_psiCached_[key]) {
797 _fillWindow_(w, t, _psiCache_[key], false); // base: no slice-specific evidence
798 _psiCached_[key] = true;
799 }
800
801 // unobserved: the base IS the answer, handed over without a copy
802 if (!_observationSlices_.contains(t)) return _psiCache_[key];
803
804 // observed: an observation multiplies ON TOP of the CPT, so the base still
805 // holds. Copying it and applying the likelihood costs one pass per clique,
806 // where a rebuild would redo the unit fill and every CPT product.
809 return _psiScratch_;
810 }
811
812 template < GUM_Numeric GUM_SCALAR >
814 const _Window_& w,
815 int t,
816 std::unordered_map< NodeId, Tensor< GUM_SCALAR > >& psi,
817 bool withTemporalEvidence) const {
818 psi.clear();
819
820 // every clique starts at the unit potential: even one that receives no
821 // factor must contribute its variables' free mass to a summed-out message
822 for (const NodeId c: w.jt.nodes()) {
823 Tensor< GUM_SCALAR > p;
824 for (const NodeId n: w.jt.clique(c))
825 p << *_varOfSlot_(w.slotOfNode[n], t);
826 p.fillWith(GUM_SCALAR(1));
827 psi.emplace(c, std::move(p));
828 }
829
830 const auto applyBase = [&](int b, NodeId owner) {
831 const std::string& base = _baseNames_[b];
832 const bool atemp = b >= static_cast< int >(_nbTemporal_);
833 const std::string name = _encode_(base, atemp ? ATEMPORAL : t);
834 const DiscreteVariable& var
835 = atemp ? _ktbn_->variable(base, ATEMPORAL) : _ktbn_->variable(base, t % _k_);
836
837 // do(base[t]=v): the point mass REPLACES the CPT, severing the node
838 // from its own causes -- an observation never does this
839 // In a base build the slice's own evidence is left out, so a temporal
840 // node keeps its CPT and no likelihood is applied; atemporal evidence is
841 // slice-independent and stays, being part of the periodic structure.
842 const bool sliceEvidence = atemp || withTemporalEvidence;
843
844 const auto itv = sliceEvidence ? _interventions_.find(name) : _interventions_.end();
845 if (itv != _interventions_.end()) {
846 psi.at(owner) *= Tensor< GUM_SCALAR >::deterministicTensor(var, itv->second);
847 } else if (atemp) {
848 psi.at(owner) *= _ktbn_->cpt(base, ATEMPORAL);
849 } else if (t <= _k_ - 1) {
850 psi.at(owner) *= _ktbn_->cpt(base, t); // initial CPT: already on ring slots
851 } else {
852 psi.at(owner) *= _buildKernel_(base, t);
853 }
854
855 // base[t] observed: pure conditioning, multiplied in ON TOP of the CPT.
856 const auto ite = sliceEvidence ? _observations_.find(name) : _observations_.end();
857 if (ite != _observations_.end()) {
858 Tensor< GUM_SCALAR > ev;
859 ev << var;
860 ev.fillWith(ite->second);
861 psi.at(owner) *= ev;
862 }
863 };
864
865 for (const auto& [c, bases]: w.factorsOf)
866 for (const int b: bases)
867 applyBase(b, c);
868 }
869
870 template < GUM_Numeric GUM_SCALAR >
872 return base == o.base && lag == o.lag;
873 }
874
875 template < GUM_Numeric GUM_SCALAR >
876 INLINE bool KTBNInference< GUM_SCALAR >::_isTemporal_(const std::string& base) const {
877 return _ktbn_->temporalVarNames().find(base) != _ktbn_->temporalVarNames().end();
878 }
879
880 template < GUM_Numeric GUM_SCALAR >
881 INLINE bool KTBNInference< GUM_SCALAR >::_isAtemporal_(const std::string& base) const {
882 return _ktbn_->atemporalVarNames().find(base) != _ktbn_->atemporalVarNames().end();
883 }
884
885 template < GUM_Numeric GUM_SCALAR >
886 INLINE const typename KTBNInference< GUM_SCALAR >::_Window_&
888 return _windows_[static_cast< std::size_t >(t < _k_ ? t : _k_)];
889 }
890
891 template < GUM_Numeric GUM_SCALAR >
893 const _Window_& w,
894 const std::unordered_map< NodeId, Tensor< GUM_SCALAR > >& psi,
895 const std::map< std::pair< NodeId, NodeId >, Tensor< GUM_SCALAR > >& msgs,
896 const Tensor< GUM_SCALAR >* inPrev,
897 const Tensor< GUM_SCALAR >* inNext,
898 NodeId c,
899 NodeId skip) const {
900 Tensor< GUM_SCALAR > out = psi.at(c);
901 for (const NodeId n: w.jt.neighbours(c)) {
902 if (n == skip) continue;
903 const auto it = msgs.find({n, c});
904 if (it != msgs.end()) out *= it->second;
905 }
906 // the two virtual leaves: past hangs off D_t, future off C_t. Leaving one
907 // out is how Shafer-Shenoy avoids echoing a message back -- no division needed
908 if (inPrev != nullptr && c == w.rootD && skip != KTBN_SKIP_PREV) out *= *inPrev;
909 if (inNext != nullptr && c == w.rootC && skip != KTBN_SKIP_NEXT) out *= *inNext;
910 return out;
911 }
912
913 template < GUM_Numeric GUM_SCALAR >
915 const _Window_& w,
916 int t,
917 const std::unordered_map< NodeId, Tensor< GUM_SCALAR > >& psi,
918 const Tensor< GUM_SCALAR >* inPrev,
919 const Tensor< GUM_SCALAR >* inNext,
920 bool distribute,
921 std::map< std::pair< NodeId, NodeId >, Tensor< GUM_SCALAR > >& msgs) const {
922 msgs.clear();
923 if (w.bfs.empty()) return;
924
925 const auto sepVars = [&](NodeId a, NodeId b) {
926 VariableSet keep;
927 for (const NodeId n: w.jt.separator(a, b))
928 keep.insert(_varOfSlot_(w.slotOfNode[n], t));
929 return keep;
930 };
931
932 // collect: reverse BFS visits every child before its parent
933 for (auto it = w.bfs.rbegin(); it != w.bfs.rend(); ++it) {
934 const NodeId j = *it;
935 if (j == w.rootC) continue;
936 const NodeId p = w.parentOf.at(j);
937 msgs[{j, p}] = _belief_(w, psi, msgs, inPrev, inNext, j, p).sumIn(sepVars(j, p));
938 }
939 if (!distribute) return;
940
941 // distribute: BFS visits every parent before its children
942 for (const NodeId j: w.bfs)
943 for (const NodeId i: w.jt.neighbours(j)) {
944 if (i == j || w.parentOf.at(i) != j) continue; // children only
945 msgs[{j, i}] = _belief_(w, psi, msgs, inPrev, inNext, j, i).sumIn(sepVars(j, i));
946 }
947 }
948
949 // ===========================================================================
950 // Inference
951 // ===========================================================================
952
953 template < GUM_Numeric GUM_SCALAR >
955 if (nbTimeSlices == 0) GUM_ERROR(InvalidArgument, "makeInference: nbTimeSlices must be >= 1.")
956
957 const int T = static_cast< int >(nbTimeSlices);
958 _horizon_ = nbTimeSlices;
959 _logObservation_ = GUM_SCALAR(0);
960 _posteriors_.clear();
961 _kernelCache_.clear(); // CPT values may have been edited since the last run
962
963 // Potential cache: same reason to clear, plus the evidence may have moved.
964 _psiCache_.assign(2 * _k_, {});
965 _psiCached_.assign(2 * _k_, false);
966 _psiScratch_.clear();
967
968 // Which slices are not the periodic ones. Decoding the (few) evidence keys
969 // costs O(#evidence); probing every slice would cost what the cache saves.
970 // Atemporal evidence decodes to ATEMPORAL and marks no slice: it applies at
971 // every t alike and so belongs to the periodic structure.
972 _observationSlices_.clear();
973 _interventionSlices_.clear();
974 for (const auto& [name, value]: _observations_) {
975 (void)value;
976 const int s = _determineNode_(name).second;
977 if (s != ATEMPORAL) _observationSlices_.insert(s);
978 }
979 for (const auto& [name, value]: _interventions_) {
980 (void)value;
981 const int s = _determineNode_(name).second;
982 if (s != ATEMPORAL) _interventionSlices_.insert(s);
983 }
985
986 const bool defaultAll = !_targeted_mode_;
987 const auto targeted = [&](const std::string& b) { return defaultAll || _targets_.contains(b); };
988 // nothing observed => backward messages are provably uniform, so the
989 // sweep is skipped and only one window is ever live
990 const bool smoothing = hasObservation();
991
992 // pre-size the series: with smoothing the slices are filled back-to-front
993 for (int i = 0; i < static_cast< int >(_baseNames_.size()); ++i) {
994 const std::string& b = _baseNames_[i];
995 if (!_requisite_[i] || !targeted(b)) continue;
996 _Series_& s = _posteriors_[b];
997 const std::size_t n
998 = (i < static_cast< int >(_nbTemporal_)) ? static_cast< std::size_t >(T) : std::size_t(1);
999 s.vars.resize(n);
1000 s.tensors.resize(n);
1001 }
1002
1003 const auto ifaceVars = [&](const std::vector< _Slot_ >& slots, int slice) {
1004 VariableSet keep;
1005 for (const auto& s: slots)
1006 keep.insert(_varOfSlot_(s, slice));
1007 return keep;
1008 };
1009
1010 const auto readPosteriors
1011 = [&](const _Window_& w,
1012 int slice,
1013 const std::unordered_map< NodeId, Tensor< GUM_SCALAR > >& psi,
1014 const std::map< std::pair< NodeId, NodeId >, Tensor< GUM_SCALAR > >& msgs,
1015 const Tensor< GUM_SCALAR >* inPrev,
1016 const Tensor< GUM_SCALAR >* inNext) {
1017 const int lastBase = (slice == 0) ? static_cast< int >(_baseNames_.size())
1018 : static_cast< int >(_nbTemporal_);
1019 for (int i = 0; i < lastBase; ++i) {
1020 if (!_requisite_[i] || !targeted(_baseNames_[i])) continue;
1021 const auto itc = w.selfClique.find(i);
1022 if (itc == w.selfClique.end()) continue;
1023 const bool atemp = i >= static_cast< int >(_nbTemporal_);
1024 VariableSet keep;
1025 keep.insert(atemp ? &_ktbn_->variable(_baseNames_[i], ATEMPORAL)
1026 : &_ktbn_->variable(_baseNames_[i], slice % _k_));
1027 Tensor< GUM_SCALAR > m
1028 = _belief_(w, psi, msgs, inPrev, inNext, itc->second, KTBN_SKIP_NONE).sumIn(keep);
1029 m.normalize();
1030 _snapshot_(_baseNames_[i], atemp ? ATEMPORAL : slice, m);
1031 }
1032 };
1033
1034 std::map< std::pair< NodeId, NodeId >, Tensor< GUM_SCALAR > > msgs;
1035
1036 // ---- forward sweep: carry m_t across each slice boundary ----------------
1037 // only the interface is retained per slice, and only if smoothing needs it
1038 std::vector< Tensor< GUM_SCALAR > > fwd;
1039 if (smoothing && T > 1) fwd.resize(static_cast< std::size_t >(T - 1));
1040
1041 Tensor< GUM_SCALAR > prev;
1042 bool hasPrev = false;
1043
1044 for (int t = 0; t < T; ++t) {
1045 const _Window_& w = _windowAt_(t);
1046 if (w.bfs.empty()) break; // nothing requisite at all
1047
1048 const auto& psi = _windowPotentials_(w, t);
1049 _propagate_(w, t, psi, hasPrev ? &prev : nullptr, nullptr, !smoothing, msgs);
1050
1051 if (!smoothing) readPosteriors(w, t, psi, msgs, hasPrev ? &prev : nullptr, nullptr);
1052
1053 Tensor< GUM_SCALAR > m
1054 = _belief_(w, psi, msgs, hasPrev ? &prev : nullptr, nullptr, w.rootC, KTBN_SKIP_NEXT)
1055 .sumIn(ifaceVars(w.Icur, t));
1056 const GUM_SCALAR mass = m.sum();
1057 if (!(mass > GUM_SCALAR(0)))
1059 "makeInference: the observations have probability 0 under this model "
1060 "(impossible at slice "
1061 << t << ").")
1062 m.scale(GUM_SCALAR(1) / mass);
1063 _logObservation_ += static_cast< GUM_SCALAR >(std::log(static_cast< double >(mass)));
1064
1065 if (t + 1 < T) {
1066 if (smoothing) fwd[static_cast< std::size_t >(t)] = m;
1067 prev = std::move(m);
1068 hasPrev = true;
1069 }
1070 }
1071
1072 // ---- backward sweep: carry r_t the other way and combine ---------------
1073 if (smoothing) {
1074 Tensor< GUM_SCALAR > nxt;
1075 bool hasNext = false;
1076 for (int t = T - 1; t >= 0; --t) {
1077 const _Window_& w = _windowAt_(t);
1078 if (w.bfs.empty()) break;
1079
1080 const auto& psi = _windowPotentials_(w, t);
1081 const Tensor< GUM_SCALAR >* inPrev
1082 = (t > 0) ? &fwd[static_cast< std::size_t >(t - 1)] : nullptr;
1083 const Tensor< GUM_SCALAR >* inNext = hasNext ? &nxt : nullptr;
1084 _propagate_(w, t, psi, inPrev, inNext, true, msgs);
1085
1086 readPosteriors(w, t, psi, msgs, inPrev, inNext);
1087
1088 if (t > 0) {
1089 Tensor< GUM_SCALAR > r = _belief_(w, psi, msgs, inPrev, inNext, w.rootD, KTBN_SKIP_PREV)
1090 .sumIn(ifaceVars(w.Iprev, t));
1091 const GUM_SCALAR mass = r.sum();
1092 // Symmetric with the forward sweep. In exact arithmetic this cannot
1093 // fire -- a null backward mass would mean P(e_{t:T} | I) = 0 for every
1094 // interface state, hence P(e) = 0, which the forward sweep just
1095 // disproved. In floating point it fires readily: the intra-window
1096 // product is formed BEFORE any normalisation, so near-deterministic
1097 // CPTs flush it to zero while the true value is merely tiny
1098 // (reproduced with transition probabilities of 1e-100 at T=4).
1099 //
1100 // Left unguarded the failure is silent and total: r stays all-zero,
1101 // becomes the next inNext, and zeroes every belief from here back to
1102 // slice 0 -- and readPosteriors' normalize() is a no-op on a zero sum,
1103 // so posterior() would return tensors of zeros looking like
1104 // distributions. Do NOT reuse the forward sweep's wording here: the
1105 // observations are *not* impossible, they underflowed.
1106 if (!(mass > GUM_SCALAR(0)))
1108 "makeInference: the backward message leaving slice "
1109 << t
1110 << " underflowed to zero. The observations are not impossible -- the "
1111 "forward sweep accepted them (log P(obs) = "
1113 << ") -- but the model's probabilities are too extreme for this horizon "
1114 "to be resolved in double precision. Soften the near-deterministic "
1115 "CPT entries, or shorten the horizon / reduce the observations.")
1116 r.scale(GUM_SCALAR(1) / mass);
1117 nxt = std::move(r);
1118 hasNext = true;
1119 }
1120 }
1121 }
1122
1123 _done_ = true;
1124 }
1125
1126 // ===========================================================================
1127 // Results
1128 // ===========================================================================
1129
1130 template < GUM_Numeric GUM_SCALAR >
1131 void KTBNInference< GUM_SCALAR >::_snapshot_(const std::string& base,
1132 int slice,
1133 const Tensor< GUM_SCALAR >& marginal) {
1134 // owned, stably-named descriptor: marginal's axis is a reused ring object
1135 // whose name isn't base[slice], so fillWith is positional, not name-matched
1136 const std::size_t idx = (slice == ATEMPORAL) ? 0u : static_cast< std::size_t >(slice);
1137 _Series_& series = _posteriors_[base];
1138 if (series.tensors.size() <= idx) {
1139 series.tensors.resize(idx + 1);
1140 series.vars.resize(idx + 1);
1141 }
1142
1143 std::unique_ptr< DiscreteVariable > outVar(_templateVar_(base, slice).clone());
1144 outVar->setName(_encode_(base, slice));
1145
1146 Tensor< GUM_SCALAR > out;
1147 out << *outVar;
1148 out.fillWith(marginal, {marginal.variablesSequence().atPos(0)->name()});
1149
1150 series.vars[idx] = std::move(outVar);
1151 series.tensors[idx] = std::move(out);
1152 }
1153
1154 template < GUM_Numeric GUM_SCALAR >
1156 KTBNInference< GUM_SCALAR >::_series_(const std::string& base) {
1157 // lazily (re)run with the last horizon; a horizon of 0 means never run
1158 if (!_done_) {
1159 if (_horizon_ == 0)
1160 GUM_ERROR(OperationNotAllowed, "call makeInference(nbTimeSlices) before querying.")
1162 }
1163 const auto it = _posteriors_.find(base);
1164 if (it == _posteriors_.end())
1165 GUM_ERROR(UndefinedElement, "'" << base << "' is not a target of this inference.")
1166 return it->second;
1167 }
1168
1169 template < GUM_Numeric GUM_SCALAR >
1170 const Tensor< GUM_SCALAR >& KTBNInference< GUM_SCALAR >::posterior(std::string_view base,
1171 int slice) {
1172 const std::string b(base);
1173 const _Series_& series = _series_(b);
1174
1175 if (_isAtemporal_(b)) {
1176 if (slice != ATEMPORAL)
1177 GUM_ERROR(InvalidArgument, "Atemporal variable '" << b << "' has no time slice.")
1178 return series.tensors.front();
1179 }
1180 if (slice < 0 || static_cast< Size >(slice) >= series.tensors.size())
1182 "Slice " << slice << " for '" << b << "' was not computed (horizon " << _horizon_
1183 << ").")
1184 return series.tensors[static_cast< std::size_t >(slice)];
1185 }
1186
1187 template < GUM_Numeric GUM_SCALAR >
1188 const Tensor< GUM_SCALAR >& KTBNInference< GUM_SCALAR >::posterior(std::string_view node_name) {
1189 const auto [b, s] = _determineNode_(std::string(node_name));
1190 return posterior(b, s);
1191 }
1192
1193 template < GUM_Numeric GUM_SCALAR >
1194 const std::vector< Tensor< GUM_SCALAR > >&
1196 return _series_(std::string(base)).tensors;
1197 }
1198
1199 template < GUM_Numeric GUM_SCALAR >
1201 if (!_done_) {
1202 if (_horizon_ == 0)
1203 GUM_ERROR(OperationNotAllowed, "call makeInference(nbTimeSlices) before querying.")
1205 }
1206 return _logObservation_;
1207 }
1208
1209 template < GUM_Numeric GUM_SCALAR >
1211 return static_cast< GUM_SCALAR >(std::exp(static_cast< double >(logObservationProbability())));
1212 }
1213
1214 // ===========================================================================
1215 // Various
1216 // ===========================================================================
1217
1218 template < GUM_Numeric GUM_SCALAR >
1219 const KTBN< GUM_SCALAR >& KTBNInference< GUM_SCALAR >::ktbn() const {
1220 return *_ktbn_;
1221 }
1222
1223 template < GUM_Numeric GUM_SCALAR >
1225 return _windows_[static_cast< std::size_t >(_k_)].jt;
1226 }
1227
1228 template < GUM_Numeric GUM_SCALAR >
1230 return static_cast< Size >(_windows_[static_cast< std::size_t >(_k_)].Icur.size());
1231 }
1232
1233 template < GUM_Numeric GUM_SCALAR >
1235 std::stringstream s;
1236 s << "KTBNInference (k=" << _k_ << ", interface=" << interfaceSize() << ")\n";
1237 s << " interventions: {";
1238 bool first = true;
1239 for (const auto& [name, val]: _interventions_) {
1240 s << (first ? "" : ", ") << "do(" << name << "=" << val << ")";
1241 first = false;
1242 }
1243 s << "}\n observations: {";
1244 first = true;
1245 for (const auto& [name, like]: _observations_) {
1246 (void)like;
1247 s << (first ? "" : ", ") << name;
1248 first = false;
1249 }
1250 s << "}\n targets: ";
1251 if (!_targeted_mode_) s << "<all bases>";
1252 else {
1253 s << "{";
1254 first = true;
1255 for (const auto& name: _targets_) {
1256 s << (first ? "" : ", ") << name;
1257 first = false;
1258 }
1259 s << "}";
1260 }
1261 s << "\n state: " << (_done_ ? "computed" : "not computed yet") << "\n";
1262 return s.str();
1263 }
1264
1265} // namespace gum
Exact inference for k-order dynamic Bayesian networks: Murphy's interface algorithm,...
const NodeSet & separator(const Edge &edge) const
returns the separator included in a given edge
void addEdge(NodeId first, NodeId second) override
inserts a new edge between two cliques
const NodeSet & clique(const NodeId idClique) const
returns the set of nodes included into a given clique
The default triangulation algorithm used by aGrUM.
Base class for discrete random variable.
virtual Size domainSize() const =0
const NodeSet & neighbours(NodeId id) const
returns the set of node neighbours to a given node
Exception : fatal (unknown ?) error.
value_type & insert(const Key &key, const Val &val)
Adds a new element (actually a copy of this element) into the hash table.
Exception: at least one argument passed to a function is not what was expected.
void _fillWindow_(const _Window_ &w, int t, std::unordered_map< NodeId, Tensor< GUM_SCALAR > > &psi, bool withTemporalEvidence=true) const
void _snapshot_(const std::string &base, int slice, const Tensor< GUM_SCALAR > &marginal)
Snapshots marginal onto an owned, stably-named descriptor and appends it to that base's series (index...
void makeInference(Size nbTimeSlices)
Runs the interface algorithm over nbTimeSlices slices ( ) and caches, for every targeted base,...
void addObservation(std::string_view base, int slice, const KTBNModality &value)
Records a hard observation .
KTBNInference(const KTBN< GUM_SCALAR > *ktbn)
Constructor.
void _markRequisite_()
Marks the requisite bases of the current run (targets, observed bases and all their ancestors) into r...
void _applyTemporalObservations_(const _Window_ &w, int t, std::unordered_map< NodeId, Tensor< GUM_SCALAR > > &psi) const
Multiplies slice t's temporal observation likelihoods into an already-built base. Atemporal ones are ...
void clearObservation()
Removes all recorded observations.
std::vector< bool > _psiCached_
std::unordered_map< NodeId, Tensor< GUM_SCALAR > > _psiScratch_
Potentials of an evidence-carrying slice, rebuilt on each visit.
void eraseIntervention(std::string_view base, int slice)
Removes a recorded intervention (silent no-op if absent).
bool isInTargetMode() const
Size _horizon_
Horizon (nbTimeSlices) of the last/next run; 0 <=> makeInference never run.
const DiscreteVariable & _templateVar_(const std::string &base, int slice) const
A representative template variable of base for domain/cloning.
std::vector< bool > _requisite_
Bases actually folded by the current run: the targets, the observed nodes and all their ancestors....
void clearTargets()
Removes all targets (restores default-all-targets mode).
void addTarget(std::string_view base)
Declares a target: a base variable whose marginals we want.
GUM_SCALAR _logObservation_
log P(observation | do) of the last run.
std::unordered_set< int > _observationSlices_
Slices carrying a temporal observation. Their potentials are the periodic ones times that slice's lik...
GUM_SCALAR logObservationProbability()
for the last run: the likelihood of the observations under the (possibly mutilated) model....
_Window_ _compileWindow_(const std::vector< _Slot_ > &Iprev, const std::vector< _Slot_ > &Icur, int t, bool withAtemporalFamilies) const
Compiles one window over the given slot set / interfaces.
std::set< std::string > _targets_
Recorded targets (base names). Empty <=> default-all-targets mode.
Tensor< GUM_SCALAR > _belief_(const _Window_ &w, const std::unordered_map< NodeId, Tensor< GUM_SCALAR > > &psi, const std::map< std::pair< NodeId, NodeId >, Tensor< GUM_SCALAR > > &msgs, const Tensor< GUM_SCALAR > *inPrev, const Tensor< GUM_SCALAR > *inNext, NodeId c, NodeId skipNeighbour) const
The belief of clique c: its potential times every message reaching it, interface messages included.
std::vector< int > _maxLag_
maxLag[i]: largest lag at which the transition kernel still consumes temporal base i – how long an oc...
std::vector< std::string > _baseNames_
All bases: temporal first (indices 0.._nbTemporal_-1), then atemporal. Window slots index into this.
bool _targeted_mode_
Whether at least one explicit target has been declared.
std::unordered_map< std::string, _Series_ > _posteriors_
Cached marginal series of the last run, keyed by base name.
const JunctionTree & windowJunctionTree() const
The junction tree of the repeating window – the one compiled from the k-slice template and re-entered...
void addObservations(const std::vector< std::pair< NodeKey, KTBNModality > > &observations)
Records several observations in one call, all-or-nothing.
const std::unordered_map< NodeId, Tensor< GUM_SCALAR > > & _windowPotentials_(const _Window_ &w, int t) const
Clique potentials of the window at slice t: every requisite family's CPT (or, under an intervention,...
void _validateNode_(const std::string &base, int slice) const
Validates that (base, slice) denotes a legal node (future slices ok).
void clearInterventions()
Removes all recorded interventions.
bool hasIntervention(std::string_view base, int slice) const
const KTBN< GUM_SCALAR > & ktbn() const
Size interfaceSize() const
Size of the forward interface of the repeating window: how many node occurrences have to cross each s...
std::string toString() const
std::vector< std::unordered_map< NodeId, Tensor< GUM_SCALAR > > > _psiCache_
Memoized clique potentials for the slices that carry no temporal evidence, indexed by psiKey(t)....
static std::string _encode_(const std::string &base, int slice)
Encodes (base, slice) -> engine name (base[slice] or bare base).
std::map< std::string, Idx > _interventions_
Recorded interventions, keyed by engine name -> forced value.
const Tensor< GUM_SCALAR > & _buildKernel_(const std::string &p, int t) const
Transition-kernel tensor of process p at slice t ( ): the template kernel remapped onto the k-DBN's o...
const KTBN< GUM_SCALAR > * _ktbn_
The k-DBN (referenced, not owned).
bool isTarget(std::string_view base) const
const DiscreteVariable * _varOfSlot_(const _Slot_ &s, int t) const
The variable a window slot stands for at absolute slice t: ring slot for a temporal base,...
GUM_SCALAR observationProbability()
, i.e. exp of logObservationProbability(). Underflows to 0 on long horizons; prefer the log form.
std::vector< _Slot_ > _interfaceAfter_(int t) const
The forward interface after slice t, as slots relative to t: every requisite occurrence at a slice s...
Size _psiKey_(int t) const
Cache slot for slice t: the initial slices keep their own, the repeating window contributes one per p...
void eraseObservation(std::string_view base, int slice)
Removes a recorded observation (silent no-op if absent).
void _propagate_(const _Window_ &w, int t, const std::unordered_map< NodeId, Tensor< GUM_SCALAR > > &psi, const Tensor< GUM_SCALAR > *inPrev, const Tensor< GUM_SCALAR > *inNext, bool distribute, std::map< std::pair< NodeId, NodeId >, Tensor< GUM_SCALAR > > &msgs) const
Shafer-Shenoy pass over a filled window. inPrev / inNext are the interface messages arriving at rootD...
const _Series_ & _series_(const std::string &base)
The cached series of a targeted base, running makeInference() lazily (with the last horizon) if out o...
std::vector< _Window_ > _windows_
The compiled windows: index t for t <= k-2 (initial), index k-1 for the repeating window,...
std::size_t _nbTemporal_
bool _done_
Whether the cached posteriors are up to date.
void addIntervention(std::string_view base, int slice, const KTBNModality &value)
Records a hard intervention .
void eraseTarget(std::string_view base)
Removes a target; when the last one is removed, default-all-targets mode is restored.
std::unordered_set< int > _interventionSlices_
Slices carrying a temporal intervention. These need a full rebuild: do(X=x) replaces the node's CPT,...
std::vector< std::string > _atemporalSorted_
int _lastConsumerSlice_(int baseIdx, int s) const
The last slice at which occurrence base[s] is still consumed (-1 if never), over both the initial fam...
void _buildWindows_()
Compiles the k window junction trees, once, from the constructor: moralise each window's families,...
std::vector< _Slot_ > _familySlots_(int baseIdx, int t) const
Parents of base at a window whose current slice is t, as slots (lag = t - parentSlice)....
std::pair< std::string, int > _determineNode_(const std::string &name) const
Cache-aware classification of an engine name -> (base, slice): a name registered as atemporal (incl....
std::map< std::pair< std::string, int >, Tensor< GUM_SCALAR > > _kernelCache_
Memoized transition kernels, keyed by (process, t % k).
int _k_
The order k, cached as int for slice arithmetic.
const std::vector< Tensor< GUM_SCALAR > > & posteriors(std::string_view base)
The whole marginal time-series of a targeted base: tensors[t] is for (a single-element vector,...
bool _isTemporal_(const std::string &base) const
std::vector< std::string > _temporalSorted_
Temporal / atemporal base names in a deterministic (sorted) order, cached once at construction (the K...
std::map< std::string, std::vector< GUM_SCALAR > > _observations_
Recorded observations, keyed by engine name -> likelihood vector (one-hot for a hard observation).
bool hasObservation(std::string_view base, int slice) const
bool hasObservation() const
std::unordered_map< std::string, int > _baseIdx_
name -> index into baseNames
static constexpr int ATEMPORAL
Convenience alias for the atemporal-slice sentinel.
const Tensor< GUM_SCALAR > & posterior(std::string_view base, int slice)
Returns .
bool _isAtemporal_(const std::string &base) const
const _Window_ & _windowAt_(int t) const
The window for absolute slice t: its own while t is inside the initial block, the repeating one (inde...
Size size() const
alias for sizeNodes
const NodeGraphPart & nodes() const
return *this as a NodeGraphPart
Exception : the element we looked for cannot be found.
Exception : operation not allowed.
Exception : out of bound.
bool contains(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:468
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:510
const CliqueGraph & junctionTree() override
returns a compatible junction tree
static Tensor< GUM_SCALAR > deterministicTensor(const DiscreteVariable &var, Idx value)
Exception : a looked-for element could not be found.
Base class for undirected graphs.
Definition undiGraph.h:130
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Definition types.h:74
Size Idx
Type for indexes.
Definition types.h:79
Size NodeId
Type for node ids.
HashTable< NodeId, VAL > NodeProperty
Property on graph elements.
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
Set< const DiscreteVariable * > VariableSet
CliqueGraph JunctionTree
a junction tree is a clique graph satisfying the running intersection property and such that no cliqu...
A cached marginal time-series for one base: owned variable descriptors paired with their marginals,...
std::vector< std::unique_ptr< DiscreteVariable > > vars
std::vector< Tensor< GUM_SCALAR > > tensors
One node of a window template: a base (index into baseNames) at a lag behind the window's current sli...
bool operator==(const _Slot_ &o) const
A compiled window: the junction tree of , rooted at the clique holding , plus everything needed to fi...
std::unordered_map< NodeId, NodeId > parentOf
NodeId rootC
clique containing the whole outgoing interface I_t
std::unordered_map< NodeId, std::vector< int > > factorsOf
clique -> base indices whose family factor is multiplied in there
std::vector< _Slot_ > Icur
std::vector< _Slot_ > Iprev
the two interfaces, as slot lists
NodeId rootD
clique containing the whole incoming interface I_{t-1}
JunctionTree jt
the junction tree over those nodes
std::vector< _Slot_ > slotOfNode
template graph NodeId -> slot it stands for
std::vector< NodeId > bfs
cliques in BFS order from rootC, and each one's parent in that rooting
std::unordered_map< int, NodeId > selfClique
base index -> clique holding that base's own slot (lag 0 / atemporal), for reading its posterior and ...
A parent's value in gum::KTBN::fillCPT(): a modality index or a modality label.
Definition KTBN.h:95
Idx toIndex(const DiscreteVariable &var) const
Resolves this value to a modality index of var, looking the label up when one was given.