aGrUM 3.1.1
a C++ library for (probabilistic) graphical models
ShaferShenoyLIMIDInference_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#pragma once
42
43
49
50#include <agrum/ID/inference/ShaferShenoyLIMIDInference.h> // to ease IDE parser
51#ifndef DOXYGEN_SHOULD_SKIP_THIS
52
53// to ease parsing by IDE
54# include <limits>
55
59
60# define GUM_SSLI_TRACE_ON(x) // GUM_TRACE(x)
61# define GUM_SSLI_TENSOR_TRACE_ON(x) // GUM_TRACE(x)
62
63namespace gum {
64
65 template < GUM_Numeric GUM_SCALAR >
67 const InfluenceDiagram< GUM_SCALAR >* infDiag) :
68 InfluenceDiagramInference< GUM_SCALAR >(infDiag) {
69 GUM_CONSTRUCTOR(ShaferShenoyLIMIDInference);
70 createReduced_();
71 }
72
73 template < GUM_Numeric GUM_SCALAR >
74 ShaferShenoyLIMIDInference< GUM_SCALAR >::~ShaferShenoyLIMIDInference() {
75 GUM_DESTRUCTOR(ShaferShenoyLIMIDInference);
76 }
77
78 template < GUM_Numeric GUM_SCALAR >
79 void ShaferShenoyLIMIDInference< GUM_SCALAR >::clear() {
80 GraphicalModelInference< GUM_SCALAR >::clear();
81 noForgettingOrder_.clear();
82 reduced_.clear();
83 reducedJunctionTree_.clear();
84 solvabilityOrder_.clear();
85 posteriors_.clear();
86 unconditionalDecisions_.clear();
87 strategies_.clear();
88 reversePartialOrder_.clear();
89 }
90
91 template < GUM_Numeric GUM_SCALAR >
92 void ShaferShenoyLIMIDInference< GUM_SCALAR >::onStateChanged_() {}
93
94 template < GUM_Numeric GUM_SCALAR >
95 void ShaferShenoyLIMIDInference< GUM_SCALAR >::onEvidenceAdded_(const NodeId id,
96 bool isHardEvidence) {
97 const InfluenceDiagram< GUM_SCALAR >& infdiag = this->influenceDiagram();
98 if (infdiag.isUtilityNode(id)) { GUM_ERROR(InvalidNode, "No evidence on a utility node.") }
99 if (infdiag.isDecisionNode(id)) {
100 if (!isHardEvidence) GUM_ERROR(InvalidNode, "No soft evidence on a decision node.")
101 }
102 }
103
104 template < GUM_Numeric GUM_SCALAR >
105 void ShaferShenoyLIMIDInference< GUM_SCALAR >::onEvidenceErased_(const NodeId id,
106 bool isHardEvidence) {}
107
108 template < GUM_Numeric GUM_SCALAR >
109 void ShaferShenoyLIMIDInference< GUM_SCALAR >::onAllEvidenceErased_(bool contains_hard_evidence) {
110 }
111
112 template < GUM_Numeric GUM_SCALAR >
113 void ShaferShenoyLIMIDInference< GUM_SCALAR >::onEvidenceChanged_(const NodeId id,
114 bool hasChangedSoftHard) {}
115
116 template < GUM_Numeric GUM_SCALAR >
117 void ShaferShenoyLIMIDInference< GUM_SCALAR >::onModelChanged_(const GraphicalModel* model) {
118 createReduced_();
119 }
120
121 template < GUM_Numeric GUM_SCALAR >
122 void ShaferShenoyLIMIDInference< GUM_SCALAR >::updateOutdatedStructure_() {
123 createReduced_();
124 }
125
126 template < GUM_Numeric GUM_SCALAR >
127 void ShaferShenoyLIMIDInference< GUM_SCALAR >::updateOutdatedTensors_() {}
128
129 template < GUM_Numeric GUM_SCALAR >
130 void ShaferShenoyLIMIDInference< GUM_SCALAR >::makeInference_() {
131 if (!isSolvable()) { GUM_ERROR(FatalError, "This LIMID/Influence Diagram is not solvable.") }
132
133 PhiNodeProperty phi;
134 PsiArcProperty psi;
135
136 GUM_SSLI_TRACE_ON("\n\n")
137
138 initializingInference_(phi, psi);
139 // message passing (using reverse order of solvabilityOrder)
140 // first collect of phis into root
141 const auto firstRootIndice = 0;
142 collectingMessage_(phi, psi, node_to_clique_[solvabilityOrder_[firstRootIndice]]);
143 deciding_(phi, psi, solvabilityOrder_[firstRootIndice]);
144
145 for (Idx nextRootIndice = 1; nextRootIndice < solvabilityOrder_.size(); nextRootIndice++) {
146 if (node_to_clique_[solvabilityOrder_[nextRootIndice - 1]]
147 != node_to_clique_[solvabilityOrder_[nextRootIndice]]) {
148 collectingToFollowingRoot_(phi,
149 psi,
150 node_to_clique_[solvabilityOrder_[nextRootIndice - 1]],
151 node_to_clique_[solvabilityOrder_[nextRootIndice]]);
152 }
153 deciding_(phi, psi, solvabilityOrder_[nextRootIndice]);
154 }
155
156 // last distribution
157 distributingMessage_(phi,
158 psi,
159 node_to_clique_[solvabilityOrder_[solvabilityOrder_.size() - 1]]);
160 computingPosteriors_(phi, psi);
161 }
162
163 template < GUM_Numeric GUM_SCALAR >
164 void ShaferShenoyLIMIDInference< GUM_SCALAR >::initializingInference_(PhiNodeProperty& phi,
165 PsiArcProperty& psi) {
166 const auto& jt = *junctionTree();
167 const auto& infdiag = this->influenceDiagram();
168 // init JT tensors and separators
169
170 for (const auto node: jt.nodes()) {
171 phi.insert(node, DecisionTensor< GUM_SCALAR >());
172 for (const auto nei: jt.neighbours(node)) {
173 psi.insert(Arc(node, nei), DecisionTensor< GUM_SCALAR >());
174 if (node < nei) { // to do it only once by edge
175 // we create the set of vars in node and nei (cached in varsSeparators_)
176 for (const auto n: jt.clique(node) * jt.clique(nei))
177 varsSeparator_.getWithDefault(Edge(node, nei), SetOfVars())
178 .insert(&(infdiag.variable(n)));
179 }
180 }
181 }
182 for (const auto node: infdiag.nodes()) {
183 const auto clik = node_to_clique_[node];
184 if (this->hasEvidence(node)) {
185 auto q = *(this->evidence()[node]);
186 phi[clik].insertProba(q.normalize());
187 }
188
189 if (infdiag.isDecisionNode(node)) {
190 if (!this->hasEvidence(node)) {
191 auto p = (Tensor< GUM_SCALAR >() << infdiag.variable(node)).fillWith(1).normalize();
192 phi[clik].insertProba(p); // WITHOUT NORMALIZATION !!!
193 }
194 } else if (infdiag.isChanceNode(node)) phi[clik].insertProba(infdiag.cpt(node));
195 else if (infdiag.isUtilityNode(node)) phi[clik].insertUtility(infdiag.utility(node));
196 else GUM_ERROR(FatalError, "Node " << node << " has no type.")
197 }
198 }
199
200 template < GUM_Numeric GUM_SCALAR >
201 void ShaferShenoyLIMIDInference< GUM_SCALAR >::_creatingJunctionTree_() {
202 const auto& infdiag = this->influenceDiagram();
203 auto moral = reduced_.moralGraph();
204
205 // once the moral graph is completed, we remove the utility nodes before
206 // triangulation
207 NodeProperty< Size > modalities;
208 for (const auto node: infdiag.nodes())
209 if (infdiag.isUtilityNode(node)) {
210 moral.eraseNode(node);
211 } else {
212 modalities.insert(node, infdiag.variable(node).domainSize());
213 }
214 DefaultTriangulation triangulation(&moral, &modalities);
215 reducedJunctionTree_ = triangulation.junctionTree();
216 _findingCliqueForEachNode_(triangulation);
217 }
218
219 template < GUM_Numeric GUM_SCALAR >
220 void ShaferShenoyLIMIDInference< GUM_SCALAR >::_findingCliqueForEachNode_(
221 DefaultTriangulation& triangulation) {
222 // indicate, for each node of the moral graph a clique in _JT_ that can
223 // contain its conditional probability table
224 const auto& infdiag = this->influenceDiagram();
225 NodeId first_eliminated_node;
226 Idx elim_number;
227 node_to_clique_.clear();
228 const std::vector< NodeId >& JT_elim_order = triangulation.eliminationOrder();
229 NodeProperty< Idx > elim_order(Size(JT_elim_order.size()));
230 for (Idx i = Idx(0), size = JT_elim_order.size(); i < size; ++i)
231 elim_order.insert(JT_elim_order[i], (int)i);
232 for (const auto node: reduced_.nodes()) {
233 if (infdiag.isUtilityNode(node)) {
234 // utility nodes are not in the junction tree but we want to associate a
235 // clique as well
236 first_eliminated_node = node;
237 elim_number = std::numeric_limits< NodeId >::max(); // an impossible elim_number;
238 } else {
239 // get the variables in the tensor of node (and its parents)
240 first_eliminated_node = node;
241 elim_number = elim_order[first_eliminated_node];
242 }
243
244 for (const auto parent: reduced_.parents(node)) {
245 if (elim_order[parent] < elim_number) {
246 elim_number = elim_order[parent];
247 first_eliminated_node = parent;
248 }
249 }
250
251 // first_eliminated_node contains the first var (node or one of its
252 // parents) eliminated => the clique created during its elimination
253 // contains node and all of its parents => it can contain the tensor
254 // assigned to the node in the BN
255 node_to_clique_.insert(node, triangulation.createdJunctionTreeClique(first_eliminated_node));
256 }
257 }
258
259 template < GUM_Numeric GUM_SCALAR >
260 std::pair< GUM_SCALAR, GUM_SCALAR > ShaferShenoyLIMIDInference< GUM_SCALAR >::MEU() {
261 if (!this->isInferenceDone()) GUM_ERROR(OperationNotAllowed, "Call MakeInference first")
262
263 const InfluenceDiagram< GUM_SCALAR >& infdiag = this->influenceDiagram();
264
265 GUM_SCALAR resmean = 0;
266 GUM_SCALAR resvar = 0;
267 for (auto node: infdiag.nodes()) {
268 if (infdiag.isUtilityNode(node)) {
269 auto p = meanVar(node);
270 resmean += p.first;
271 resvar += p.second;
272 }
273 }
274 return std::pair< GUM_SCALAR, GUM_SCALAR >(resmean, resvar);
275 }
276
277 template < GUM_Numeric GUM_SCALAR >
279 ShaferShenoyLIMIDInference< GUM_SCALAR >::optimalDecision(NodeId decisionId) {
280 if (!this->isInferenceDone()) GUM_ERROR(OperationNotAllowed, "Call MakeInference first")
281
282 const InfluenceDiagram< GUM_SCALAR >& infdiag = this->influenceDiagram();
283 if (!infdiag.isDecisionNode(decisionId))
285 infdiag.variable(decisionId).name()
286 << "(" << decisionId << ") is not a decision node.")
287
288 return strategies_[decisionId];
289 }
290
291 template < GUM_Numeric GUM_SCALAR >
292 bool ShaferShenoyLIMIDInference< GUM_SCALAR >::isSolvable() const {
293 return (!solvabilityOrder_.empty());
294 }
295
296 template < GUM_Numeric GUM_SCALAR >
297 void ShaferShenoyLIMIDInference< GUM_SCALAR >::createReduced_() {
298 // from LIMIDS of decision Problems, Lauritzen et Nilsson, 1999
299 reduced_.clear();
300 reducedJunctionTree_.clear();
301 solvabilityOrder_.clear();
302 reversePartialOrder_.clear();
303 posteriors_.clear();
304 unconditionalDecisions_.clear();
305 strategies_.clear();
306 const InfluenceDiagram< GUM_SCALAR >& infdiag = this->influenceDiagram();
307
308 NodeSet utilities;
309
310 // build reduced_
311 for (auto node: infdiag.nodes()) {
312 reduced_.addNodeWithId(node);
313 if (infdiag.isUtilityNode(node)) { utilities.insert(node); }
314 }
315
316 for (const auto& arc: infdiag.arcs())
317 reduced_.addArc(arc.tail(), arc.head());
318
319 _completingNoForgettingAssumption_();
320 _creatingPartialOrder_(utilities);
321 _checkingSolvability_(utilities);
322 if (isSolvable()) {
323 _reducingLIMID_();
324 _creatingJunctionTree_();
325 }
326
327 this->setState_(GraphicalModelInference< GUM_SCALAR >::StateOfInference::OutdatedStructure);
328 }
329
330 template < GUM_Numeric GUM_SCALAR >
331 void ShaferShenoyLIMIDInference< GUM_SCALAR >::_completingNoForgettingAssumption_() {
332 // force no forgetting if necessary
333 if (hasNoForgettingAssumption()) {
334 auto last = *(noForgettingOrder_.begin());
335 for (auto node: noForgettingOrder_)
336 if (node == last) // first one
337 continue;
338 else { // we deal with last->node
339 // adding the whole family of last as parents of node
340 if (!reduced_.existsArc(last, node)) { reduced_.addArc(last, node); }
341 for (auto par: reduced_.parents(last)) {
342 if (!reduced_.existsArc(par, node)) reduced_.addArc(par, node);
343 }
344 last = node;
345 }
346 }
347 }
348
349 template < GUM_Numeric GUM_SCALAR >
350 void ShaferShenoyLIMIDInference< GUM_SCALAR >::_checkingSolvability_(const NodeSet& utilities) {
351 if (hasNoForgettingAssumption()) {
352 solvabilityOrder_ = noForgettingOrder_;
353 std::reverse(solvabilityOrder_.begin(), solvabilityOrder_.end());
354 return;
355 }
356
357 solvabilityOrder_.clear();
358 for (const auto& sen: reversePartialOrder()) {
359 NodeSet tobetested = sen;
360 while (!tobetested.empty()) {
361 bool foundOne = false;
362 for (const auto& node: tobetested) {
363 const auto us = utilities * reduced_.descendants(node);
364 NodeSet decs;
365 for (const auto dec: tobetested)
366 if (dec != node) decs += reduced_.family(dec);
367 if (reduced_.dSeparation(decs, us, reduced_.family(node))) {
368 solvabilityOrder_.push_back(node);
369 foundOne = true;
370 tobetested.erase(node);
371 break;
372 }
373 }
374 if (!foundOne) { // no solvability
375 solvabilityOrder_.clear();
376 return;
377 }
378 }
379 }
380 }
381
382 template < GUM_Numeric GUM_SCALAR >
383 void ShaferShenoyLIMIDInference< GUM_SCALAR >::_reducingLIMID_() {
384 for (const auto& sen: reversePartialOrder_) {
385 for (auto n: sen) {
386 for (auto p: nonRequisiteNodes_(n))
387 reduced_.eraseArc(Arc(p, n));
388 }
389 }
390 }
391
392 template < GUM_Numeric GUM_SCALAR >
393 void ShaferShenoyLIMIDInference< GUM_SCALAR >::_creatingPartialOrder_(const NodeSet& utilities) {
394 const InfluenceDiagram< GUM_SCALAR >& infdiag = this->influenceDiagram();
395 NodeProperty< Size > level;
396
397 for (const auto& node: utilities)
398 level.insert(node, 0); // utility node is level 0
399
400 // creating the partial order
401 Size max_level = 0;
402 reversePartialOrder_.clear();
403 reversePartialOrder_.resize(infdiag.size());
404 NodeSet currents;
405 for (auto node: infdiag.nodes()) {
406 if (infdiag.isUtilityNode(node)) continue;
407 if (reduced_.children(node).isSubsetOrEqual(utilities)) {
408 currents.clear();
409 currents.insert(node);
410 level.insert(node, 0);
411 while (!currents.empty()) {
412 NodeId elt = *(currents.begin());
413 currents.erase(elt);
414
415 if (infdiag.isDecisionNode(elt)) reversePartialOrder_[level[elt]].insert(elt);
416
417 for (auto parent: reduced_.parents(elt)) {
418 Size lev = 0;
419 Size newl;
420 bool ok_to_add = true;
421 for (auto child: reduced_.children(parent)) {
422 if (!level.exists(child)) {
423 ok_to_add = false;
424 break;
425 }
426 newl = level[child];
427 if (infdiag.isDecisionNode(child)) newl += 1;
428 if (lev < newl) lev = newl;
429 }
430 if (ok_to_add) {
431 currents.insert(parent);
432 if (level.exists(parent)) {
433 if (level[parent] != lev)
435 "Trying to set level[" << parent << "] to level=" << lev
436 << " but already is " << level[parent]);
437 } else {
438 level.insert(parent, lev);
439 }
440
441 if (max_level < lev) max_level = lev;
442 }
443 }
444 }
445 }
446 }
447 Size levmax = 0;
448 for (const auto& k: reversePartialOrder_) {
449 if (k.empty()) break;
450 levmax++;
451 }
452 reversePartialOrder_.resize(levmax);
453 }
454
455 template < GUM_Numeric GUM_SCALAR >
456 std::vector< NodeSet > ShaferShenoyLIMIDInference< GUM_SCALAR >::reversePartialOrder() const {
457 return reversePartialOrder_;
458 }
459
460 template < GUM_Numeric GUM_SCALAR >
461 bool ShaferShenoyLIMIDInference< GUM_SCALAR >::hasNoForgettingAssumption() const {
462 return !noForgettingOrder_.empty();
463 }
464
465 template < GUM_Numeric GUM_SCALAR >
466 void ShaferShenoyLIMIDInference< GUM_SCALAR >::addNoForgettingAssumption(
467 const std::vector< std::string >& names) {
468 addNoForgettingAssumption(this->influenceDiagram().ids(names));
469 }
470
471 template < GUM_Numeric GUM_SCALAR >
472 void ShaferShenoyLIMIDInference< GUM_SCALAR >::addNoForgettingAssumption(
473 const std::vector< NodeId >& ids) {
474 const auto& infdiag = this->influenceDiagram();
475 for (const auto node: ids) {
476 if (!infdiag.exists(node)) GUM_ERROR(NotFound, node << " is not a NodeId")
477 if (!infdiag.isDecisionNode(node))
479 "Node " << node << " (" << infdiag.variable(node).name()
480 << ") is not a decision node");
481 }
482 if (infdiag.decisionNodeSize() != ids.size())
483 GUM_ERROR(SizeError, "Some decision nodes are missing in the sequence " << ids)
484
485 noForgettingOrder_ = ids;
486 createReduced_();
487 }
488
489 template < GUM_Numeric GUM_SCALAR >
490 NodeSet ShaferShenoyLIMIDInference< GUM_SCALAR >::nonRequisiteNodes_(NodeId d) const {
491 const InfluenceDiagram< GUM_SCALAR >& infdiag = this->influenceDiagram();
492
493 if (!infdiag.isDecisionNode(d)) GUM_ERROR(TypeError, d << " is not a decision node")
494
495 NodeSet res;
496 if (reduced_.parents(d).empty()) return res;
497
498 NodeSet descUs;
499 for (const auto n: reduced_.descendants(d))
500 if (infdiag.isUtilityNode(n)) descUs.insert(n);
501
502 NodeSet cumul{descUs};
503 cumul << d;
504 auto g = reduced_.moralizedAncestralGraph(cumul);
505
506 NodeSet family{reduced_.parents(d)};
507 family << d;
508 bool notReq;
509 for (const auto p: reduced_.parents(d)) {
510 notReq = true;
511 for (const auto u: descUs) {
512 if (g.hasUndirectedPath(p, u, family)) {
513 notReq = false;
514 break;
515 }
516 }
517 if (notReq) res << p;
518 }
519 return res;
520 }
521
522 template < GUM_Numeric GUM_SCALAR >
523 InfluenceDiagram< GUM_SCALAR > ShaferShenoyLIMIDInference< GUM_SCALAR >::reducedLIMID() const {
524 const auto& infdiag = this->influenceDiagram();
525 InfluenceDiagram< GUM_SCALAR > res;
526 for (auto node: infdiag.nodes()) {
527 if (infdiag.isChanceNode(node)) res.addChanceNode(infdiag.variable(node), node);
528 else if (infdiag.isDecisionNode(node)) res.addDecisionNode(infdiag.variable(node), node);
529 else // (infdiag.isUtilityNode(node))
530 res.addUtilityNode(infdiag.variable(node), node);
531 }
532
533 for (const auto& arc: reduced_.arcs()) {
534 res.addArc(arc.tail(), arc.head());
535 }
536
537 for (auto node: infdiag.nodes()) {
538 if (infdiag.isChanceNode(node)) res.cpt(node).fillWith(infdiag.cpt(node));
539 else if (infdiag.isUtilityNode(node)) res.utility(node).fillWith(infdiag.utility(node));
540 }
541
542 // Tensors !!!
543 return res;
544 }
545
546 template < GUM_Numeric GUM_SCALAR >
547 const JunctionTree* ShaferShenoyLIMIDInference< GUM_SCALAR >::junctionTree() const {
548 if (!isSolvable()) { GUM_ERROR(FatalError, "This LIMID/Influence Diagram is not solvable.") }
549 return &reducedJunctionTree_;
550 }
551
552 template < GUM_Numeric GUM_SCALAR >
553 void ShaferShenoyLIMIDInference< GUM_SCALAR >::collectingMessage_(PhiNodeProperty& phi,
554 PsiArcProperty& psi,
555 const NodeId rootClique) {
556 const auto& jt = *junctionTree();
557 GUM_SSLI_TRACE_ON("COLLECTING TO " << rootClique << ":"
558 << this->influenceDiagram().names(jt.clique(rootClique)))
559
560 std::function< void(NodeId, NodeId) > parcours = [&](NodeId node, NodeId toRoot) {
561 for (const auto nei: jt.neighbours(node)) {
562 if (nei != toRoot) parcours(nei, node);
563 }
564 transmittingMessage_(phi, psi, node, toRoot);
565 GUM_SSLI_TRACE_ON(" " << node << "->" << toRoot << " transmitted")
566 };
567
568 for (const auto nei: jt.neighbours(rootClique)) {
569 parcours(nei, rootClique);
570 }
571 }
572
573 template < GUM_Numeric GUM_SCALAR >
574 void ShaferShenoyLIMIDInference< GUM_SCALAR >::collectingToFollowingRoot_(PhiNodeProperty& phi,
575 PsiArcProperty& psi,
576 NodeId fromClique,
577 NodeId toClique) {
578 GUM_SSLI_TRACE_ON("COLLECTING FROM ROOT " << fromClique << " TO FOLLOWING ROOT " << toClique)
579 const auto& jt = *junctionTree();
580
581 std::function< bool(NodeId, NodeId, NodeId) > revparcours
582 = [&](NodeId node, NodeId from, NodeId target) {
583 if (node == target) return true;
584
585 bool found = false;
586 NodeId prec;
587 for (const auto nei: jt.neighbours(node)) {
588 if (nei != from)
589 if (revparcours(nei, node, target)) {
590 prec = nei;
591 found = true;
592 break;
593 }
594 }
595 if (found) { transmittingMessage_(phi, psi, prec, node); }
596 return found;
597 };
598
599 revparcours(toClique, std::numeric_limits< NodeId >::max(), fromClique);
600 }
601
602 template < GUM_Numeric GUM_SCALAR >
603 void ShaferShenoyLIMIDInference< GUM_SCALAR >::deciding_(PhiNodeProperty& phi,
604 PsiArcProperty& psi,
605 NodeId decisionNode) {
606 const auto& infdiag = this->influenceDiagram();
607 GUM_SSLI_TRACE_ON("DECIDING for " << infdiag.variable(decisionNode).name())
608
609 auto& decision = strategies_.getWithDefault(decisionNode, Tensor< GUM_SCALAR >());
610
611 if (this->hasHardEvidence(decisionNode)) {
612 decision = *(this->evidence()[decisionNode]);
613 } else {
614 DecisionTensor< double > dp;
615 dp = integrating_(phi, psi, node_to_clique_[decisionNode]);
616 GUM_SSLI_TENSOR_TRACE_ON(dp)
617
618 SetOfVars sev;
619 sev.insert(&infdiag.variable(decisionNode));
620 for (const auto parent: reduced_.parents(decisionNode)) {
621 sev.insert(&infdiag.variable(parent));
622 }
623 dp = dp ^ sev;
624 GUM_SSLI_TENSOR_TRACE_ON(dp)
625
626 // SPECIAL CASE FOR DETERMINISTIC DECISION
627 sev.erase(&infdiag.variable(decisionNode)); // only the parents in sev
628 if (sev.size() == 0) { // deterministic decision node
629 unconditionalDecisions_.set(decisionNode, dp);
630 } else if (dp.probPot.sumIn(sev).normalize().max()
631 == 1) { // with deterministic posterior probability
632 // we can use marginalization because we know that dp is deterministic
633 unconditionalDecisions_.set(
634 decisionNode,
635 DecisionTensor< double >(dp.probPot.sumOut(sev), dp.utilPot.sumOut(sev)));
636 }
637 decision = dp.utilPot.putFirst(&infdiag.variable(decisionNode));
638
639 binarizingMax_(decision, dp.probPot);
640 GUM_SSLI_TENSOR_TRACE_ON(decision)
641 }
642 GUM_SSLI_TENSOR_TRACE_ON(phi[node_to_clique_[decisionNode]])
643 phi[node_to_clique_[decisionNode]].insertProba(decision);
644 GUM_SSLI_TENSOR_TRACE_ON(phi[node_to_clique_[decisionNode]])
645 }
646
647 template < GUM_Numeric GUM_SCALAR >
648 void ShaferShenoyLIMIDInference< GUM_SCALAR >::binarizingMax_(
649 const Tensor< GUM_SCALAR >& decision,
650 const Tensor< GUM_SCALAR >& proba) const { // compute the decisions (as maxEU)
651 Instantiation I(decision);
652 const auto& firstvar = decision.variable(0);
653 for (I.setFirst(); !I.end(); I.incNotVar(firstvar)) {
654 I.setFirstVar(firstvar);
655 while (proba[I] == 0) {
656 I.incVar(firstvar);
657 if (I.end()) { // for non valid proba, we keep the first value (by
658 // default)²
659 I.setFirstVar(firstvar);
660 break;
661 }
662 }
663 // we found a non null value of proba
664 Idx argm = I.val(firstvar);
665 GUM_SCALAR umax = decision[I];
666 GUM_SCALAR pmax = proba[I];
667 for (I.incVar(firstvar); !I.end(); I.incVar(firstvar)) {
668 // lexicographical order on (u,p)
669 if (proba[I] > 0) {
670 if ((umax < decision[I]) || ((umax == decision[I]) && (pmax < proba[I]))) {
671 umax = decision[I];
672 pmax = proba[I];
673 argm = I.val(firstvar);
674 }
675 }
676 }
677 for (I.setFirstVar(firstvar); !I.end(); I.incVar(firstvar))
678 decision.set(I, 0);
679 I.chgVal(firstvar, argm);
680 decision.set(I, 1);
681 }
682 }
683
684 template < GUM_Numeric GUM_SCALAR >
685 void ShaferShenoyLIMIDInference< GUM_SCALAR >::distributingMessage_(PhiNodeProperty& phi,
686 PsiArcProperty& psi,
687 NodeId rootClique) {
688 const auto& jt = *junctionTree();
689 GUM_SSLI_TRACE_ON("DISTRIBUTING FROM " << rootClique << ":"
690 << this->influenceDiagram().names(jt.clique(rootClique)))
691
692 std::function< void(NodeId, NodeId) > parcours = [&](NodeId node, NodeId from) {
693 transmittingFinalMessage_(phi, psi, from, node);
694 auto res = integrating_(phi, psi, node);
695
696 res.probPot
698 psi[gum::Arc(node, from)].probPot);
699 res.utilPot = res.utilPot - psi[gum::Arc(node, from)].utilPot;
700
701 phi.set(node, res);
702 GUM_SSLI_TRACE_ON(" -> phi[" << node << "] updated")
703 for (const auto nei: jt.neighbours(node)) {
704 if (nei != from) parcours(nei, node);
705 }
706 };
707
708 phi.set(rootClique, integrating_(phi, psi, rootClique));
709 GUM_SSLI_TRACE_ON(" -> phi[" << rootClique << "] updated")
710 GUM_SSLI_TENSOR_TRACE_ON(phi[rootClique])
711
712 for (const auto nei: jt.neighbours(rootClique)) {
713 parcours(nei, rootClique);
714 }
715 }
716
717 template < GUM_Numeric GUM_SCALAR >
718 void ShaferShenoyLIMIDInference< GUM_SCALAR >::transmittingFinalMessage_(PhiNodeProperty& phi,
719 PsiArcProperty& psi,
720 NodeId fromClique,
721 NodeId toClique) {
722 GUM_SSLI_TRACE_ON(fromClique << "->" << toClique << " [final]")
723 // no need to integrate phi : already done
724 psi.set(Arc(fromClique, toClique),
725 phi[fromClique] ^ varsSeparator_[Edge(fromClique, toClique)]);
726 }
727
728 template < GUM_Numeric GUM_SCALAR >
729 void ShaferShenoyLIMIDInference< GUM_SCALAR >::transmittingMessage_(PhiNodeProperty& phi,
730 PsiArcProperty& psi,
731 NodeId fromClique,
732 NodeId toClique) {
733 GUM_SSLI_TRACE_ON(fromClique << "->" << toClique)
734 psi.set(Arc(fromClique, toClique),
735 integrating_(phi, psi, fromClique, toClique)
736 ^ varsSeparator_[Edge(fromClique, toClique)]);
737 }
738
739 template < GUM_Numeric GUM_SCALAR >
740 DecisionTensor< double >
741 ShaferShenoyLIMIDInference< GUM_SCALAR >::integrating_(const PhiNodeProperty& phi,
742 const PsiArcProperty& psi,
743 NodeId inClique,
744 NodeId except) const {
745 const auto& jt = *junctionTree();
746 GUM_SSLI_TRACE_ON(" integrating (except from "
747 << except << ") in " << inClique << ":"
748 << this->influenceDiagram().names(jt.clique(inClique)))
749 DecisionTensor< double > res = phi[inClique];
750 for (const auto nei: jt.neighbours(inClique))
751 if (nei != except) res *= psi[Arc(nei, inClique)];
752
753 return res;
754 }
755
756 template < GUM_Numeric GUM_SCALAR >
757 DecisionTensor< double >
758 ShaferShenoyLIMIDInference< GUM_SCALAR >::integrating_(const PhiNodeProperty& phi,
759 const PsiArcProperty& psi,
760 NodeId inClique) const {
761 const auto& jt = *junctionTree();
762 GUM_SSLI_TRACE_ON(" integrating in " << inClique << ":"
763 << this->influenceDiagram().names(jt.clique(inClique)))
764 DecisionTensor< double > res = phi[inClique];
765
766 for (const auto nei: jt.neighbours(inClique))
767 res *= psi[Arc(nei, inClique)];
768
769 return res;
770 }
771
772 template < GUM_Numeric GUM_SCALAR >
773 void ShaferShenoyLIMIDInference< GUM_SCALAR >::computingPosteriors_(const PhiNodeProperty& phi,
774 const PsiArcProperty& psi) {
775 NodeProperty< DecisionTensor< double > > finalphis;
776
777 const auto& infdiag = this->influenceDiagram();
778 posteriors_.clear();
779 strategies_.clear();
780 for (const auto node: infdiag.nodes()) {
781 const auto clik = node_to_clique_[node];
782 // if (!finalphis.exists(clik)) finalphis.insert(clik, integrating_(phi, psi, clik));
783 // const auto& finalphi = finalphis[clik];
784 const auto& finalphi = phi[clik];
785 GUM_SSLI_TRACE_ON("posterior for " << infdiag.variable(node).name())
786
787 DecisionTensor< GUM_SCALAR > res;
788
789 if (infdiag.isChanceNode(node)) {
790 SetOfVars sev;
791 sev.insert(&infdiag.variable(node));
792 res = finalphi ^ sev;
793 const auto f = res.probPot.sum();
794 res.probPot.scale(1 / f);
795 } else if (infdiag.isDecisionNode(node)) {
796 SetOfVars sev;
797 sev.insert(&infdiag.variable(node));
798 SetOfVars family = sev;
799 for (const auto par: reduced_.parents(node)) {
800 if (infdiag.isChanceNode(par)) family.insert(&infdiag.variable(par));
801 }
802 // GCC 16 false positive: deep inlining confuses heap-allocated internals
803 // of DecisionTensor with the stack object bounds
804# pragma GCC diagnostic push
805# pragma GCC diagnostic ignored "-Warray-bounds"
806 const auto dp = finalphi ^ family;
807
808 gum::Tensor< double > decision = dp.utilPot.putFirst(&infdiag.variable(node));
809 binarizingMax_(decision, dp.probPot);
810 strategies_.insert(node, decision);
811 res = dp ^ sev;
812# pragma GCC diagnostic pop
813 res.probPot.normalize();
814 if (unconditionalDecisions_.exists(node)) {
815 res.utilPot = unconditionalDecisions_[node].utilPot;
816 }
817 } else { // utility
818 SetOfVars family;
819
820 family.insert(&infdiag.variable(node));
821 for (const auto par: reduced_.parents(node)) {
822 family.insert(&infdiag.variable(par));
823 }
824 res = finalphi ^ family;
825
826 res.probPot.normalize();
827 res.utilPot = infdiag.utility(node);
828 }
829
830 posteriors_.set(node, res);
831 }
832 }
833
834 template < GUM_Numeric GUM_SCALAR >
835 const Tensor< GUM_SCALAR >& ShaferShenoyLIMIDInference< GUM_SCALAR >::posterior(NodeId node) {
836 return posteriors_[node].probPot;
837 }
838
839 template < GUM_Numeric GUM_SCALAR >
840 const Tensor< GUM_SCALAR >&
841 ShaferShenoyLIMIDInference< GUM_SCALAR >::posteriorUtility(NodeId node) {
842 return posteriors_[node].utilPot;
843 }
844
845 template < GUM_Numeric GUM_SCALAR >
846 std::pair< GUM_SCALAR, GUM_SCALAR >
847 ShaferShenoyLIMIDInference< GUM_SCALAR >::meanVar(NodeId node) {
848 return posteriors_[node].meanVar();
849 }
850
851 template < GUM_Numeric GUM_SCALAR >
852 DAG ShaferShenoyLIMIDInference< GUM_SCALAR >::reducedGraph() const {
853 return reduced_;
854 }
855
856 template < GUM_Numeric GUM_SCALAR >
858 ShaferShenoyLIMIDInference< GUM_SCALAR >::optimalDecision(std::string_view decisionName) {
859 return optimalDecision(this->influenceDiagram().idFromName(decisionName));
860 }
861
862 template < GUM_Numeric GUM_SCALAR >
863 const Tensor< GUM_SCALAR >&
864 ShaferShenoyLIMIDInference< GUM_SCALAR >::posterior(std::string_view name) {
865 return posterior(this->influenceDiagram().idFromName(name));
866 }
867
868 template < GUM_Numeric GUM_SCALAR >
869 const Tensor< GUM_SCALAR >&
870 ShaferShenoyLIMIDInference< GUM_SCALAR >::posteriorUtility(std::string_view name) {
871 return posteriorUtility(this->influenceDiagram().idFromName(name));
872 }
873
874 template < GUM_Numeric GUM_SCALAR >
875 std::pair< GUM_SCALAR, GUM_SCALAR >
876 ShaferShenoyLIMIDInference< GUM_SCALAR >::meanVar(std::string_view name) {
877 return meanVar(this->influenceDiagram().idFromName(name));
878 }
879
880} /* namespace gum */
881
882#endif /* DOXYGEN_SHOULD_SKIP_THIS */
Implementation of an influence diagram inference algorithm based upon Shaffer-Shenoy's one for bayes ...
The base class for all directed edges.
static Tensor< GUM_SCALAR > divideEvenZero(const Tensor< GUM_SCALAR > &p1, const Tensor< GUM_SCALAR > &p2)
Exception : fatal (unknown ?) error.
<agrum/ID/inference/influenceDiagramInference.h>
Class representing an Influence Diagram.
Exception : node does not exist.
Exception : the element we looked for cannot be found.
Exception : operation not allowed.
void resize(Size new_capacity)
Changes the size of the underlying hash table containing the set.
Definition set_tpl.h:439
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:510
ShaferShenoyLIMIDInference(const InfluenceDiagram< GUM_SCALAR > *infDiag)
Default constructor.
Exception : problem with size.
aGrUM's Tensor is a multi-dimensional array with tensor operators.
Definition tensor.h:85
Exception : wrong type for this operation.
This file contains abstract class definitions influence diagrams inference classes.
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
NodeSet family(const G &g, NodeId id)
Returns the family of id : { id } ∪ parents(id).
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
CliqueGraph JunctionTree
a junction tree is a clique graph satisfying the running intersection property and such that no cliqu...
Header of the Tensor class.