aGrUM 3.1.1
a C++ library for (probabilistic) graphical models
ShaferShenoyInference_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
51
52#include <agrum/BN/inference/ShaferShenoyInference.h> // to ease IDE parser
53#ifndef DOXYGEN_SHOULD_SKIP_THIS
54# include <algorithm>
55
63
64namespace gum {
65 // default constructor
66 template < GUM_Numeric GUM_SCALAR >
69 RelevantTensorsFinderType relevant_type,
70 FindBarrenNodesType barren_type,
71 bool use_binary_join_tree) :
72 JointTargetedInference< GUM_SCALAR >(BN), EvidenceInference< GUM_SCALAR >(BN),
73 _use_binary_join_tree_(use_binary_join_tree) {
74 // sets the relevant tensor and the barren nodes finding algorithm
75 _findRelevantTensors_
76 = &ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation2_;
77 setRelevantTensorsFinderType(relevant_type);
78 setFindBarrenNodesType(barren_type);
79
80 // create a default triangulation (the user can change it afterwards)
81 _triangulation_ = new DefaultTriangulation;
82
83 // for debugging purposes
84 GUM_CONSTRUCTOR(ShaferShenoyInference);
85 }
86
87 // destructor
88 template < GUM_Numeric GUM_SCALAR >
89 ShaferShenoyInference< GUM_SCALAR >::~ShaferShenoyInference() {
90 // remove all the tensors created during the last message passing
91 for (const auto& pot: _arc_to_created_tensors_)
92 delete pot.second;
93
94 // remove all the tensors in _clique_ss_tensor_ that do not belong
95 // to _clique_tensors_ : in this case, those tensors have been
96 // created by combination of the corresponding list of tensors in
97 // _clique_tensors_. In other words, the size of this list is strictly
98 // greater than 1.
99 for (auto pot: _clique_ss_tensor_) {
100 if (_clique_tensors_[pot.first].size() > 1) delete pot.second;
101 }
102
103 for (auto potset: _clique_tensors_) {
104 for (auto pot: potset.second)
105 delete pot;
106 }
107
108 // remove all the posteriors computed
109 for (const auto& pot: _target_posteriors_)
110 delete pot.second;
111 for (const auto& pot: _joint_target_posteriors_)
112 delete pot.second;
113
114 // remove the junction tree and the triangulation algorithm
115 if (_JT_ != nullptr) delete _JT_;
116 if (_junctionTree_ != nullptr) delete _junctionTree_;
117 delete _triangulation_;
118
119 // for debugging purposes
120 GUM_DESTRUCTOR(ShaferShenoyInference);
121 }
122
124 template < GUM_Numeric GUM_SCALAR >
125 void ShaferShenoyInference< GUM_SCALAR >::setTriangulation(
126 const Triangulation& new_triangulation) {
127 delete _triangulation_;
128 _triangulation_ = new_triangulation.newFactory();
129 _is_new_jt_needed_ = true;
130 this->setOutdatedStructureState_();
131 }
132
134 template < GUM_Numeric GUM_SCALAR >
135 const JoinTree* ShaferShenoyInference< GUM_SCALAR >::joinTree() {
136 if (_is_new_jt_needed_) _createNewJT_();
137
138 return _JT_;
139 }
140
142 template < GUM_Numeric GUM_SCALAR >
143 const JunctionTree* ShaferShenoyInference< GUM_SCALAR >::junctionTree() {
144 if (_is_new_jt_needed_) _createNewJT_();
145
146 return _junctionTree_;
147 }
148
150 template < GUM_Numeric GUM_SCALAR >
151 void ShaferShenoyInference< GUM_SCALAR >::setRelevantTensorsFinderType(
152 RelevantTensorsFinderType type) {
153 if (type != _find_relevant_tensor_type_) {
154 switch (type) {
155 case RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS :
156 _findRelevantTensors_
157 = &ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation2_;
158 break;
159
160 case RelevantTensorsFinderType::DSEP_BAYESBALL_NODES :
161 _findRelevantTensors_
162 = &ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation_;
163 break;
164
165 case RelevantTensorsFinderType::DSEP_KOLLER_FRIEDMAN_2009 :
166 _findRelevantTensors_
167 = &ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation3_;
168 break;
169
170 case RelevantTensorsFinderType::FIND_ALL :
171 _findRelevantTensors_ = &ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsGetAll_;
172 break;
173
174 default :
176 "setRelevantTensorsFinderType for type " << (unsigned int)type
177 << " is not implemented yet");
178 }
179
180 _find_relevant_tensor_type_ = type;
181
182 // indicate that all messages need be reconstructed to take into account
183 // the change in d-separation analysis
184 _invalidateAllMessages_();
185 }
186 }
187
189 template < GUM_Numeric GUM_SCALAR >
190 void ShaferShenoyInference< GUM_SCALAR >::_setProjectionFunction_(
191 Tensor< GUM_SCALAR > (*proj)(const Tensor< GUM_SCALAR >&, const gum::VariableSet&)) {
192 _projection_op_ = proj;
193
194 // indicate that all messages need be reconstructed to take into account
195 // the change in of the projection operator
196 _invalidateAllMessages_();
197 }
198
200 template < GUM_Numeric GUM_SCALAR >
201 void ShaferShenoyInference< GUM_SCALAR >::_setCombinationFunction_(
202 Tensor< GUM_SCALAR > (*comb)(const Tensor< GUM_SCALAR >&, const Tensor< GUM_SCALAR >&)) {
203 _combination_op_ = comb;
204
205 // indicate that all messages need be reconstructed to take into account
206 // the change of the combination operator
207 _invalidateAllMessages_();
208 }
209
211 template < GUM_Numeric GUM_SCALAR >
212 void ShaferShenoyInference< GUM_SCALAR >::_invalidateAllMessages_() {
213 // remove all the messages computed
214 for (auto& pot: _separator_tensors_)
215 pot.second = nullptr;
216
217 for (auto& mess_computed: _messages_computed_)
218 mess_computed.second = false;
219
220 // remove all the created tensors kept on the arcs
221 for (const auto& pot: _arc_to_created_tensors_)
222 if (pot.second != nullptr) delete pot.second;
223 _arc_to_created_tensors_.clear();
224
225 // remove all the posteriors
226 for (const auto& pot: _target_posteriors_)
227 delete pot.second;
228 _target_posteriors_.clear();
229 for (const auto& pot: _joint_target_posteriors_)
230 delete pot.second;
231 _joint_target_posteriors_.clear();
232
233 // indicate that new messages need be computed
234 if (this->isInferenceReady() || this->isInferenceDone()) this->setOutdatedTensorsState_();
235 }
236
238 template < GUM_Numeric GUM_SCALAR >
239 void ShaferShenoyInference< GUM_SCALAR >::setFindBarrenNodesType(FindBarrenNodesType type) {
240 if (type != _barren_nodes_type_) {
241 // WARNING: if a new type is added here, method _createJT_ should certainly
242 // be updated as well, in particular its step 2.
243 switch (type) {
244 case FindBarrenNodesType::FIND_BARREN_NODES :
245 case FindBarrenNodesType::FIND_NO_BARREN_NODES : break;
246
247 default :
249 "setFindBarrenNodesType for type " << (unsigned int)type
250 << " is not implemented yet");
251 }
252
253 _barren_nodes_type_ = type;
254
255 // tensorly, we may need to reconstruct a junction tree
256 this->setOutdatedStructureState_();
257 }
258 }
259
261 template < GUM_Numeric GUM_SCALAR >
262 void ShaferShenoyInference< GUM_SCALAR >::onEvidenceAdded_(const NodeId id, bool isHardEvidence) {
263 // if we have a new hard evidence, this modifies the undigraph over which
264 // the join tree is created. This is also the case if id is not a node of
265 // of the undigraph
266 if (isHardEvidence || !_graph_.exists(id)) _is_new_jt_needed_ = true;
267 else {
268 try {
269 _evidence_changes_.insert(id, EvidenceChangeType::EVIDENCE_ADDED);
270 } catch (DuplicateElement const&) {
271 // here, the evidence change already existed. This necessarily means
272 // that the current saved change is an EVIDENCE_ERASED. So if we
273 // erased the evidence and added some again, this corresponds to an
274 // EVIDENCE_MODIFIED
275 _evidence_changes_[id] = EvidenceChangeType::EVIDENCE_MODIFIED;
276 }
277 }
278 }
279
281 template < GUM_Numeric GUM_SCALAR >
282 void ShaferShenoyInference< GUM_SCALAR >::onEvidenceErased_(const NodeId id,
283 bool isHardEvidence) {
284 // if we delete a hard evidence, this modifies the undigraph over which
285 // the join tree is created.
286 if (isHardEvidence) _is_new_jt_needed_ = true;
287 else {
288 try {
289 _evidence_changes_.insert(id, EvidenceChangeType::EVIDENCE_ERASED);
290 } catch (DuplicateElement const&) {
291 // here, the evidence change already existed and it is necessarily an
292 // EVIDENCE_ADDED or an EVIDENCE_MODIFIED. So, if the evidence has
293 // been added and is now erased, this is similar to not having created
294 // it. If the evidence was only modified, it already existed in the
295 // last inference and we should now indicate that it has been removed.
296 if (_evidence_changes_[id] == EvidenceChangeType::EVIDENCE_ADDED)
297 _evidence_changes_.erase(id);
298 else _evidence_changes_[id] = EvidenceChangeType::EVIDENCE_ERASED;
299 }
300 }
301 }
302
304 template < GUM_Numeric GUM_SCALAR >
305 void ShaferShenoyInference< GUM_SCALAR >::onAllEvidenceErased_(bool has_hard_evidence) {
306 if (has_hard_evidence || !this->hardEvidenceNodes().empty()) _is_new_jt_needed_ = true;
307 else {
308 for (const auto node: this->softEvidenceNodes()) {
309 try {
310 _evidence_changes_.insert(node, EvidenceChangeType::EVIDENCE_ERASED);
311 } catch (DuplicateElement const&) {
312 // here, the evidence change already existed and it is necessarily an
313 // EVIDENCE_ADDED or an EVIDENCE_MODIFIED. So, if the evidence has
314 // been added and is now erased, this is similar to not having created
315 // it. If the evidence was only modified, it already existed in the
316 // last inference and we should now indicate that it has been removed.
317 if (_evidence_changes_[node] == EvidenceChangeType::EVIDENCE_ADDED)
318 _evidence_changes_.erase(node);
319 else _evidence_changes_[node] = EvidenceChangeType::EVIDENCE_ERASED;
320 }
321 }
322 }
323 }
324
326 template < GUM_Numeric GUM_SCALAR >
327 void ShaferShenoyInference< GUM_SCALAR >::onEvidenceChanged_(const NodeId id,
328 bool hasChangedSoftHard) {
329 if (hasChangedSoftHard) _is_new_jt_needed_ = true;
330 else {
331 try {
332 _evidence_changes_.insert(id, EvidenceChangeType::EVIDENCE_MODIFIED);
333 } catch (DuplicateElement const&) {
334 // here, the evidence change already existed and it is necessarily an
335 // EVIDENCE_ADDED. So we should keep this state to indicate that this
336 // evidence is new w.r.t. the last inference
337 }
338 }
339 }
340
342 template < GUM_Numeric GUM_SCALAR >
343 void ShaferShenoyInference< GUM_SCALAR >::onModelChanged_(const GraphicalModel* bn) {}
344
346 template < GUM_Numeric GUM_SCALAR >
347 void ShaferShenoyInference< GUM_SCALAR >::onMarginalTargetAdded_(const NodeId id) {
348 // if the graph does not contain the node, either this is due to the fact that
349 // the node has received a hard evidence or because it was d-separated from the
350 // target nodes during the last inference. In the latter case, we should change
351 // the graph, hence we should recompute the JT
352 if (!_graph_.exists(id) && !_hard_ev_nodes_.contains(id)) { _is_new_jt_needed_ = true; }
353 }
354
356 template < GUM_Numeric GUM_SCALAR >
357 void ShaferShenoyInference< GUM_SCALAR >::onMarginalTargetErased_(const NodeId id) {}
358
360 template < GUM_Numeric GUM_SCALAR >
361 void ShaferShenoyInference< GUM_SCALAR >::onJointTargetAdded_(const NodeSet& set) {
362 // if there is no current joint tree, obviously, we need one.
363 if (_JT_ == nullptr) {
364 _is_new_jt_needed_ = true;
365 return;
366 }
367
368 // here, we will remove from set the nodes that received hard evidence and try
369 // to find a clique in the current _JT_ that can contain the resulting set. If
370 // we cannot find one, we must recompute the _JT_
371 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
372 int elim_number = std::numeric_limits< int >::max();
373 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
374 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
375 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
376 elim_order.insert(JT_elim_order[i], (int)i);
377 NodeSet unobserved_set(set.size());
378 for (const auto node: set) {
379 if (!_graph_.exists(node)) {
380 if (!_hard_ev_nodes_.contains(node)) {
381 _is_new_jt_needed_ = true;
382 return;
383 }
384 } else {
385 unobserved_set.insert(node);
386 if (elim_order[node] < elim_number) {
387 elim_number = elim_order[node];
388 first_eliminated_node = node;
389 }
390 }
391 }
392
393 if (!unobserved_set.empty()) {
394 // here, first_eliminated_node contains the first var (node or one of its
395 // parents) eliminated => the clique created during its elimination
396 // should contain all the nodes in unobserved_set
397 const auto clique_id = _node_to_clique_[first_eliminated_node];
398 const auto& clique = _JT_->clique(clique_id);
399 for (const auto node: unobserved_set) {
400 if (!clique.contains(node)) {
401 _is_new_jt_needed_ = true;
402 return;
403 }
404 }
405 }
406 }
407
409 template < GUM_Numeric GUM_SCALAR >
410 void ShaferShenoyInference< GUM_SCALAR >::onJointTargetErased_(const NodeSet& set) {}
411
413 template < GUM_Numeric GUM_SCALAR >
414 void ShaferShenoyInference< GUM_SCALAR >::onAllMarginalTargetsAdded_() {
415 for (const auto node: this->BN().internalDag()) {
416 // if the graph does not contain the node, either this is due to the fact
417 // that the node has received a hard evidence or because it was d-separated
418 // from the target nodes during the last inference. In the latter case, we
419 // should change the graph, hence we should recompute the JT
420 if (!_graph_.exists(node) && !_hard_ev_nodes_.contains(node)) {
421 _is_new_jt_needed_ = true;
422 return;
423 }
424 }
425 }
426
428 template < GUM_Numeric GUM_SCALAR >
429 void ShaferShenoyInference< GUM_SCALAR >::onAllMarginalTargetsErased_() {}
430
432 template < GUM_Numeric GUM_SCALAR >
433 void ShaferShenoyInference< GUM_SCALAR >::onAllJointTargetsErased_() {}
434
436 template < GUM_Numeric GUM_SCALAR >
437 void ShaferShenoyInference< GUM_SCALAR >::onAllTargetsErased_() {}
438
439 // check whether a new junction tree is really needed for the next inference
440 template < GUM_Numeric GUM_SCALAR >
441 bool ShaferShenoyInference< GUM_SCALAR >::_isNewJTNeeded_() const {
442 // if we do not have a JT or if _new_jt_needed_ is set to true, then
443 // we know that we need to create a new join tree
444 if ((_JT_ == nullptr) || _is_new_jt_needed_) return true;
445
446 // if some targets do not belong to the join tree and, consequently, to the
447 // undirected graph that was used to construct the join tree, then we need
448 // to create a new JT. This situation may occur if we constructed the
449 // join tree after pruning irrelevant/barren nodes from the BN.
450 // However, note that the nodes that received hard evidence do not belong to
451 // the graph and, therefore, should not be taken into account
452 const auto& hard_ev_nodes = this->hardEvidenceNodes();
453 for (const auto node: this->targets()) {
454 if (!_graph_.exists(node) && !hard_ev_nodes.exists(node)) return true;
455 }
456
457 // now, do the same for the joint targets
458 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
459 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
460 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
461 elim_order.insert(JT_elim_order[i], (int)i);
462 NodeSet unobserved_set;
463
464 for (const auto& joint_target: this->jointTargets()) {
465 // here, we need to check that at least one clique contains all the
466 // nodes of the joint target.
467 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
468 int elim_number = std::numeric_limits< int >::max();
469 unobserved_set.clear();
470 for (const auto node: joint_target) {
471 if (!_graph_.exists(node)) {
472 if (!hard_ev_nodes.exists(node)) return true;
473 } else {
474 unobserved_set.insert(node);
475 if (elim_order[node] < elim_number) {
476 elim_number = elim_order[node];
477 first_eliminated_node = node;
478 }
479 }
480 }
481 if (!unobserved_set.empty()) {
482 // here, first_eliminated_node contains the first var (node or one of its
483 // parents) eliminated => the clique created during its elimination
484 // should contain all the nodes in unobserved_set
485 const auto clique_id = _node_to_clique_[first_eliminated_node];
486 const auto& clique = _JT_->clique(clique_id);
487 for (const auto node: unobserved_set) {
488 if (!clique.contains(node)) return true;
489 }
490 }
491 }
492
493 // if some new evidence have been added on nodes that do not belong
494 // to _graph_, then we tensorly have to reconstruct the join tree
495 for (const auto& change: _evidence_changes_) {
496 if ((change.second == EvidenceChangeType::EVIDENCE_ADDED) && !_graph_.exists(change.first))
497 return true;
498 }
499
500 // here, the current JT is exactly what we need for the next inference
501 return false;
502 }
503
505 template < GUM_Numeric GUM_SCALAR >
506 void ShaferShenoyInference< GUM_SCALAR >::_createNewJT_() {
507 // to create the JT, we first create the moral graph of the BN in the
508 // following way in order to take into account the barren nodes and the
509 // nodes that received evidence:
510 // 1/ we create an undirected graph containing only the nodes and no edge
511 // 2/ if we take into account barren nodes, remove them from the graph
512 // 3/ if we take d-separation into account, remove the d-separated nodes
513 // 4/ add edges so that each node and its parents in the BN form a clique
514 // 5/ add edges so that joint targets form a clique of the moral graph
515 // 6/ remove the nodes that received hard evidence (by step 4/, their
516 // parents are linked by edges, which is necessary for inference)
517 //
518 // At the end of step 6/, we have our moral graph and we can triangulate it
519 // to get the new junction tree
520
521 // 1/ create an undirected graph containing only the nodes and no edge
522 const auto& bn = this->BN();
523 _graph_.clear();
524 for (const auto node: bn.internalDag())
525 _graph_.addNodeWithId(node);
526
527 // identify the target nodes
528 NodeSet target_nodes = this->targets();
529 for (const auto& nodeset: this->jointTargets()) {
530 target_nodes += nodeset;
531 }
532
533 // 2/ if we wish to exploit barren nodes, we shall remove them from the
534 // BN. To do so: we identify all the nodes that are not targets and have
535 // received no evidence and such that their descendants are neither
536 // targets nor evidence nodes. Such nodes can be safely discarded from
537 // the BN without altering the inference output
538 if ((this->nbrTargets() != bn.internalDag().size())
539 && (_barren_nodes_type_ == FindBarrenNodesType::FIND_BARREN_NODES)) {
540 // check that all the nodes are not targets, otherwise, there is no
541 // barren node
542 if (target_nodes.size() != bn.size()) {
543 BarrenNodesFinder finder(&bn.internalDag());
544 finder.setTargets(&target_nodes);
545
546 NodeSet evidence_nodes(this->evidence().size());
547 for (const auto& pair: this->evidence()) {
548 evidence_nodes.insert(pair.first);
549 }
550 finder.setEvidence(&evidence_nodes);
551
552 NodeSet barren_nodes = finder.barrenNodes();
553
554 // remove the barren nodes from the moral graph
555 for (const auto node: barren_nodes) {
556 _graph_.eraseNode(node);
557 }
558 }
559 }
560
561 // 3/ if we wish to exploit d-separation, remove all the nodes that are
562 // d-separated from our targets. Of course, if all the nodes are targets,
563 // no need to perform a d-separation analysis
564 if (this->nbrTargets() != bn.internalDag().size()) {
565 NodeSet requisite_nodes;
566 bool dsep_analysis = false;
567 switch (_find_relevant_tensor_type_) {
568 case RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS :
569 case RelevantTensorsFinderType::DSEP_BAYESBALL_NODES : {
570 BayesBall::requisiteNodes(bn.internalDag(),
571 target_nodes,
572 this->hardEvidenceNodes(),
573 this->softEvidenceNodes(),
574 requisite_nodes);
575 dsep_analysis = true;
576 } break;
577
578 case RelevantTensorsFinderType::DSEP_KOLLER_FRIEDMAN_2009 : {
579 dSeparationAlgorithm dsep;
580 dsep.requisiteNodes(bn.internalDag(),
581 target_nodes,
582 this->hardEvidenceNodes(),
583 this->softEvidenceNodes(),
584 requisite_nodes);
585 dsep_analysis = true;
586 } break;
587
588 case RelevantTensorsFinderType::FIND_ALL : break;
589
590 default : GUM_ERROR(FatalError, "not implemented yet")
591 }
592
593 // remove all the nodes that are not requisite
594 if (dsep_analysis) {
595 for (auto iter = _graph_.beginSafe(); iter != _graph_.endSafe(); ++iter) {
596 if (!requisite_nodes.contains(*iter) && !this->hardEvidenceNodes().contains(*iter)) {
597 _graph_.eraseNode(*iter);
598 }
599 }
600 }
601 }
602
603 // 4/ add edges so that each node and its parents in the BN form a clique
604 for (const auto node: _graph_) {
605 const NodeSet& parents = bn.parents(node);
606 for (auto iter1 = parents.cbegin(); iter1 != parents.cend(); ++iter1) {
607 // before adding an edge between node and its parent, check that the
608 // parent belongs to the graph. Actually, when d-separated nodes are
609 // removed, it may be the case that the parents of hard evidence nodes
610 // are removed. But the latter still exist in the graph.
611 if (_graph_.existsNode(*iter1)) {
612 _graph_.addEdge(*iter1, node);
613
614 auto iter2 = iter1;
615 for (++iter2; iter2 != parents.cend(); ++iter2) {
616 // before adding an edge, check that both extremities belong to
617 // the graph. Actually, when d-separated nodes are removed, it may
618 // be the case that the parents of hard evidence nodes are removed.
619 // But the latter still exist in the graph.
620 if (_graph_.existsNode(*iter2)) _graph_.addEdge(*iter1, *iter2);
621 }
622 }
623 }
624 }
625
626 // 5/ if there exist some joint targets, we shall add new edges
627 // into the moral graph in order to ensure that there exists a clique
628 // containing each joint
629 for (const auto& nodeset: this->jointTargets()) {
630 for (auto iter1 = nodeset.cbegin(); iter1 != nodeset.cend(); ++iter1) {
631 auto iter2 = iter1;
632 for (++iter2; iter2 != nodeset.cend(); ++iter2) {
633 _graph_.addEdge(*iter1, *iter2);
634 }
635 }
636 }
637
638 // 6/ remove all the nodes that received hard evidence
639 _hard_ev_nodes_ = this->hardEvidenceNodes();
640 for (const auto node: _hard_ev_nodes_) {
641 _graph_.eraseNode(node);
642 }
643
644
645 // now, we can compute the new junction tree. To speed-up computations
646 // (essentially, those of a distribution phase), we construct from this
647 // junction tree a binary join tree
648 if (_JT_ != nullptr) delete _JT_;
649 if (_junctionTree_ != nullptr) delete _junctionTree_;
650
651 _triangulation_->setGraph(&_graph_, &(this->domainSizes()));
652 const JunctionTree& triang_jt = _triangulation_->junctionTree();
653 if (_use_binary_join_tree_) {
654 BinaryJoinTreeConverterDefault bjt_converter;
655 NodeSet emptyset;
656 _JT_ = new CliqueGraph(bjt_converter.convert(triang_jt, this->domainSizes(), emptyset));
657 } else {
658 _JT_ = new CliqueGraph(triang_jt);
659 }
660 _junctionTree_ = new CliqueGraph(triang_jt);
661
662
663 // indicate, for each node of the moral graph, a clique in _JT_ that can
664 // contain its conditional probability table
665 _node_to_clique_.clear();
666 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
667 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
668 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
669 elim_order.insert(JT_elim_order[i], (int)i);
670 const DAG& dag = bn.internalDag();
671 for (const auto node: _graph_) {
672 // get the variables in the tensor of node (and its parents)
673 NodeId first_eliminated_node = node;
674 int elim_number = elim_order[first_eliminated_node];
675
676 for (const auto parent: dag.parents(node)) {
677 if (_graph_.existsNode(parent) && (elim_order[parent] < elim_number)) {
678 elim_number = elim_order[parent];
679 first_eliminated_node = parent;
680 }
681 }
682
683 // first_eliminated_node contains the first var (node or one of its
684 // parents) eliminated => the clique created during its elimination
685 // contains node and all of its parents => it can contain the tensor
686 // assigned to the node in the BN
687 _node_to_clique_.insert(node,
688 _triangulation_->createdJunctionTreeClique(first_eliminated_node));
689 }
690
691 // do the same for the nodes that received hard evidence. Here, we only store
692 // the nodes for which at least one parent belongs to _graph_ (otherwise
693 // their CPT is just a constant real number).
694 for (const auto node: _hard_ev_nodes_) {
695 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
696 int elim_number = std::numeric_limits< int >::max();
697
698 for (const auto parent: dag.parents(node)) {
699 if (_graph_.exists(parent) && (elim_order[parent] < elim_number)) {
700 elim_number = elim_order[parent];
701 first_eliminated_node = parent;
702 }
703 }
704
705 // first_eliminated_node contains the first var (node or one of its
706 // parents) eliminated => the clique created during its elimination
707 // contains node and all of its parents => it can contain the tensor
708 // assigned to the node in the BN
709 if (elim_number != std::numeric_limits< int >::max()) {
710 _node_to_clique_.insert(node,
711 _triangulation_->createdJunctionTreeClique(first_eliminated_node));
712 }
713 }
714
715 // indicate for each joint_target a clique that contains it
716 _joint_target_to_clique_.clear();
717 for (const auto& set: this->jointTargets()) {
718 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
719 int elim_number = std::numeric_limits< int >::max();
720
721 // do not take into account the nodes that received hard evidence
722 // (since they do not belong to the join tree)
723 for (const auto node: set) {
724 if (!_hard_ev_nodes_.contains(node)) {
725 // the clique we are looking for is the one that was created when
726 // the first element of nodeset was eliminated
727 if (elim_order[node] < elim_number) {
728 elim_number = elim_order[node];
729 first_eliminated_node = node;
730 }
731 }
732 }
733
734 if (elim_number != std::numeric_limits< int >::max()) {
735 _joint_target_to_clique_.insert(
736 set,
737 _triangulation_->createdJunctionTreeClique(first_eliminated_node));
738 }
739 }
740
741 // compute the roots of _JT_'s connected components
742 _computeJoinTreeRoots_();
743
744 // remove all the tensors stored into the cliques. Note that these include
745 // the CPTs resulting from the projections of hard evidence as well as the
746 // CPTs of the soft evidence
747 for (const auto& pot: _clique_ss_tensor_) {
748 if (_clique_tensors_[pot.first].size() > 1) delete pot.second;
749 }
750 _clique_ss_tensor_.clear();
751 for (const auto& potlist: _clique_tensors_)
752 for (const auto pot: potlist.second)
753 delete pot;
754 _clique_tensors_.clear();
755
756 // remove all the tensors created during the last inference
757 for (const auto& pot: _arc_to_created_tensors_)
758 delete pot.second;
759 _arc_to_created_tensors_.clear();
760
761 // remove all the tensors created to take into account hard evidence
762 // during the last inference (they have already been deleted from memory
763 // by the clearing of _clique_tensors_).
764 _node_to_hard_ev_projected_CPTs_.clear();
765
766 // remove all the soft evidence.
767 _node_to_soft_evidence_.clear();
768
769 // create empty tensor lists into the cliques of the joint tree as well
770 // as empty lists of evidence
771 _ScheduleMultiDimSet_ empty_set;
772 for (const auto node: *_JT_) {
773 _clique_tensors_.insert(node, empty_set);
774 _clique_ss_tensor_.insert(node, nullptr);
775 }
776
777 // remove all the constants created due to projections of CPTs that were
778 // defined over only hard evidence nodes
779 _constants_.clear();
780
781 // create empty messages and indicate that no message has been computed yet
782 _separator_tensors_.clear();
783 _messages_computed_.clear();
784 for (const auto& edge: _JT_->edges()) {
785 const Arc arc1(edge.first(), edge.second());
786 _separator_tensors_.insert(arc1, nullptr);
787 _messages_computed_.insert(arc1, false);
788 const Arc arc2(edge.second(), edge.first());
789 _separator_tensors_.insert(arc2, nullptr);
790 _messages_computed_.insert(arc2, false);
791 }
792
793 // remove all the posteriors computed so far
794 for (const auto& pot: _target_posteriors_)
795 delete pot.second;
796 _target_posteriors_.clear();
797 for (const auto& pot: _joint_target_posteriors_)
798 delete pot.second;
799 _joint_target_posteriors_.clear();
800
801 // here, we determine whether we should use schedules during the inference.
802 // the rule is: if the sum of the domain sizes of the cliques is greater
803 // than a threshold, use schedules
804 double overall_size = 0;
805 for (const auto clique: *_JT_) {
806 double clique_size = 1.0;
807 for (const auto node: _JT_->clique(clique))
808 clique_size *= this->domainSizes()[node];
809 overall_size += clique_size;
810 }
811 _use_schedules_ = (overall_size > _schedule_threshold_);
812
813 // we shall now add all the tensors of the soft evidence to the cliques
814 const NodeProperty< const Tensor< GUM_SCALAR >* >& evidence = this->evidence();
815 for (const auto node: this->softEvidenceNodes()) {
816 if (auto ptr_clique = _node_to_clique_.tryGet(node)) {
817 auto ev_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(*evidence[node], false);
818 _node_to_soft_evidence_.insert(node, ev_pot);
819 _clique_tensors_[*ptr_clique].insert(ev_pot);
820 }
821 }
822
823 // put all the CPTs of the Bayes net nodes into the cliques
824 // here, beware: all the tensors that are defined over some nodes
825 // including hard evidence must be projected so that these nodes are
826 // removed from the tensor
827 if (_use_schedules_) {
828 Schedule schedule;
829 _initializeJTCliques_(schedule);
830 } else {
831 _initializeJTCliques_();
832 }
833
834
835 // indicate that the data structures are up to date.
836 _evidence_changes_.clear();
837 _is_new_jt_needed_ = false;
838 }
839
841 template < GUM_Numeric GUM_SCALAR >
842 void ShaferShenoyInference< GUM_SCALAR >::_initializeJTCliques_() {
843 const auto& bn = this->BN();
844 const DAG& dag = bn.internalDag();
845
846 // put all the CPTs of the Bayes net nodes into the cliques
847 // here, beware: all the tensors that are defined over some nodes
848 // including hard evidence must be projected so that these nodes are
849 // removed from the tensor
850 const NodeProperty< const Tensor< GUM_SCALAR >* >& evidence = this->evidence();
851 const NodeProperty< Idx >& hard_evidence = this->hardEvidence();
852
853 for (const auto node: dag) {
854 if (_graph_.exists(node) || _hard_ev_nodes_.contains(node)) {
855 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node);
856
857 // get the list of nodes with hard evidence in cpt
858 NodeSet hard_nodes;
859 const auto& variables = cpt.variablesSequence();
860 bool graph_contains_nodes = false;
861 for (const auto var: variables) {
862 NodeId xnode = bn.nodeId(*var);
863 if (_hard_ev_nodes_.contains(xnode)) hard_nodes.insert(xnode);
864 else if (_graph_.exists(xnode)) graph_contains_nodes = true;
865 }
866
867 // if hard_nodes contains hard evidence nodes, perform a projection
868 // and insert the result into the appropriate clique, else insert
869 // directly cpt into the clique
870 if (hard_nodes.empty()) {
871 auto sched_cpt = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(cpt, false);
872 _clique_tensors_[_node_to_clique_[node]].insert(sched_cpt);
873 } else {
874 // marginalize out the hard evidence nodes: if the cpt is defined
875 // only over nodes that received hard evidence, do not consider it
876 // as a tensor anymore but as a constant
877 // TODO substitute constants by 0-dimensional tensors
878 if (hard_nodes.size() == variables.size()) {
879 Instantiation inst(cpt);
880 for (Size i = 0; i < hard_nodes.size(); ++i) {
881 inst.chgVal(*variables[i], hard_evidence[bn.nodeId(*(variables[i]))]);
882 }
883 _constants_.insert(node, cpt.get(inst));
884 } else {
885 // here, we have a CPT defined over some nodes that received hard
886 // evidence and other nodes that did not receive it. If none of the
887 // latter belong to the graph, then the CPT is useless for inference
888 if (!graph_contains_nodes) continue;
889
890 // prepare the projection with a combine and project instance
891 gum::VariableSet hard_variables;
892 _TensorSet_ marg_cpt_set(1 + hard_nodes.size());
893 marg_cpt_set.insert(&cpt);
894 for (const auto xnode: hard_nodes) {
895 marg_cpt_set.insert(evidence[xnode]);
896 hard_variables.insert(&(bn.variable(xnode)));
897 }
898
899 // perform the combination of those tensors and their projection
900 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
901 _combination_op_,
902 _projection_op_);
903
904 _TensorSet_ new_cpt_list = combine_and_project.execute(marg_cpt_set, hard_variables);
905
906 // there should be only one tensor in new_cpt_list
907 if (new_cpt_list.size() != 1) {
908 for (const auto pot: new_cpt_list)
909 if (!marg_cpt_set.contains(pot)) delete pot;
910
912 "the projection of a tensor containing " << "hard evidence is empty!");
913 }
914 auto new_pot = const_cast< Tensor< GUM_SCALAR >* >(*(new_cpt_list.begin()));
915 auto projected_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*new_pot));
916 delete new_pot;
917
918 _clique_tensors_[_node_to_clique_[node]].insert(projected_pot);
919 _node_to_hard_ev_projected_CPTs_.insert(node, projected_pot);
920 }
921 }
922 }
923 }
924
925 // now, in _clique_tensors_, for each clique, we have the list of
926 // tensors that must be combined in order to produce the Shafer-Shenoy's
927 // tensor stored into the clique. So, perform this combination and
928 // store the result in _clique_ss_tensor_
929 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
930 for (const auto& xpotset: _clique_tensors_) {
931 const auto& potset = xpotset.second;
932 if (potset.size() > 0) {
933 // here, there will be an entry in _clique_ss_tensor_
934 // If there is only one element in potset, this element shall be
935 // stored into _clique_ss_tensor_, else all the elements of potset
936 // shall be combined and their result shall be stored
937 if (potset.size() == 1) {
938 _clique_ss_tensor_[xpotset.first] = *(potset.cbegin());
939 } else {
940 _TensorSet_ p_potset(potset.size());
941 for (const auto pot: potset)
942 p_potset.insert(
943 &(static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot)->multiDim()));
944
945 Tensor< GUM_SCALAR >* joint
946 = const_cast< Tensor< GUM_SCALAR >* >(fast_combination.execute(p_potset));
947 _clique_ss_tensor_[xpotset.first]
948 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*joint));
949 delete joint;
950 }
951 }
952 }
953 }
954
956 template < GUM_Numeric GUM_SCALAR >
957 void ShaferShenoyInference< GUM_SCALAR >::_initializeJTCliques_(Schedule& schedule) {
958 const auto& bn = this->BN();
959 const DAG& dag = bn.internalDag();
960
961 // put all the CPTs of the Bayes net nodes into the cliques
962 // here, beware: all the tensors that are defined over some nodes
963 // including hard evidence must be projected so that these nodes are
964 // removed from the tensor
965 const NodeProperty< const Tensor< GUM_SCALAR >* >& evidence = this->evidence();
966 const NodeProperty< Idx >& hard_evidence = this->hardEvidence();
967
968 for (const auto node: dag) {
969 if (_graph_.exists(node) || _hard_ev_nodes_.contains(node)) {
970 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node);
971
972 // get the list of nodes with hard evidence in cpt
973 NodeSet hard_nodes;
974 const auto& variables = cpt.variablesSequence();
975 bool graph_contains_nodes = false;
976 for (const auto var: variables) {
977 NodeId xnode = bn.nodeId(*var);
978 if (_hard_ev_nodes_.contains(xnode)) hard_nodes.insert(xnode);
979 else if (_graph_.exists(xnode)) graph_contains_nodes = true;
980 }
981
982 // if hard_nodes contains hard evidence nodes, perform a projection
983 // and insert the result into the appropriate clique, else insert
984 // directly cpt into the clique
985 if (hard_nodes.empty()) {
986 auto sched_cpt = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(cpt, false);
987 _clique_tensors_[_node_to_clique_[node]].insert(sched_cpt);
988 } else {
989 // marginalize out the hard evidence nodes: if the cpt is defined
990 // only over nodes that received hard evidence, do not consider it
991 // as a tensor anymore but as a constant
992 // TODO substitute constants by 0-dimensional tensors
993 if (hard_nodes.size() == variables.size()) {
994 Instantiation inst(cpt);
995 for (Size i = 0; i < hard_nodes.size(); ++i) {
996 inst.chgVal(*variables[i], hard_evidence[bn.nodeId(*(variables[i]))]);
997 }
998 _constants_.insert(node, cpt.get(inst));
999 } else {
1000 // here, we have a CPT defined over some nodes that received hard
1001 // evidence and other nodes that did not receive it. If none of the
1002 // latter belong to the graph, then the CPT is useless for inference
1003 if (!graph_contains_nodes) continue;
1004
1005 // prepare the projection with a combine and project instance
1006 gum::VariableSet hard_variables;
1007 _ScheduleMultiDimSet_ marg_cpt_set(1 + hard_nodes.size());
1008 const IScheduleMultiDim* sched_cpt
1009 = schedule.insertTable< Tensor< GUM_SCALAR > >(cpt, false);
1010 marg_cpt_set.insert(sched_cpt);
1011
1012 for (const auto xnode: hard_nodes) {
1013 const IScheduleMultiDim* pot
1014 = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[xnode], false);
1015 marg_cpt_set.insert(pot);
1016 hard_variables.insert(&(bn.variable(xnode)));
1017 }
1018
1019 // perform the combination of those tensors and their projection
1020 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
1021 _combination_op_,
1022 _projection_op_);
1023
1024 _ScheduleMultiDimSet_ new_cpt_list
1025 = combine_and_project.schedule(schedule, marg_cpt_set, hard_variables);
1026
1027 // there should be only one tensor in new_cpt_list
1028 if (new_cpt_list.size() != 1) {
1030 "the projection of a tensor containing " << "hard evidence is empty!");
1031 }
1032 auto projected_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1033 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1034 *new_cpt_list.begin()));
1035 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(projected_pot))
1036 ->makeResultsPersistent(true);
1037 _clique_tensors_[_node_to_clique_[node]].insert(projected_pot);
1038 _node_to_hard_ev_projected_CPTs_.insert(node, projected_pot);
1039 }
1040 }
1041 }
1042 }
1043 this->scheduler().execute(schedule);
1044
1045 // now, in _clique_tensors_, for each clique, we have the list of
1046 // tensors that must be combined in order to produce the Shafer-Shenoy's
1047 // tensor stored into the clique. So, perform this combination and
1048 // store the result in _clique_ss_tensor_
1049 schedule.clear();
1050 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1051 for (const auto& xpotset: _clique_tensors_) {
1052 const auto& potset = xpotset.second;
1053 if (potset.size() > 0) {
1054 // here, there will be an entry in _clique_ss_tensor_
1055 // If there is only one element in potset, this element shall be
1056 // stored into _clique_ss_tensor_, else all the elements of potset
1057 // shall be combined and their result shall be stored
1058 if (potset.size() == 1) {
1059 _clique_ss_tensor_[xpotset.first] = *(potset.cbegin());
1060 } else {
1061 // add the tables to combine into the schedule
1062 for (const auto pot: potset) {
1063 schedule.emplaceScheduleMultiDim(*pot);
1064 }
1065
1066 auto joint = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1067 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1068 fast_combination.schedule(schedule, potset)));
1069 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(joint))
1070 ->makeResultsPersistent(true);
1071 _clique_ss_tensor_[xpotset.first] = joint;
1072 }
1073 }
1074 }
1075 this->scheduler().execute(schedule);
1076 }
1077
1079 template < GUM_Numeric GUM_SCALAR >
1080 void ShaferShenoyInference< GUM_SCALAR >::updateOutdatedStructure_() {
1081 // check if a new JT is really needed. If so, create it
1082 if (_isNewJTNeeded_()) {
1083 _createNewJT_();
1084 } else {
1085 // here, we can answer the next queries without reconstructing all the
1086 // junction tree. All we need to do is to indicate that we should
1087 // update the tensors and messages for these queries
1088 updateOutdatedTensors_();
1089 }
1090 }
1091
1093 template < GUM_Numeric GUM_SCALAR >
1094 void ShaferShenoyInference< GUM_SCALAR >::_diffuseMessageInvalidations_(
1095 NodeId from_id,
1096 NodeId to_id,
1097 NodeSet& invalidated_cliques) {
1098 // invalidate the current clique
1099 invalidated_cliques.insert(to_id);
1100
1101 // invalidate the current arc
1102 const Arc arc(from_id, to_id);
1103 bool& message_computed = _messages_computed_[arc];
1104 if (message_computed) {
1105 message_computed = false;
1106 _separator_tensors_[arc] = nullptr;
1107 if (_arc_to_created_tensors_.exists(arc)) {
1108 delete _arc_to_created_tensors_[arc];
1109 _arc_to_created_tensors_.erase(arc);
1110 }
1111
1112 // go on with the diffusion
1113 for (const auto node_id: _JT_->neighbours(to_id)) {
1114 if (node_id != from_id) _diffuseMessageInvalidations_(to_id, node_id, invalidated_cliques);
1115 }
1116 }
1117 }
1118
1121 template < GUM_Numeric GUM_SCALAR >
1122 void ShaferShenoyInference< GUM_SCALAR >::updateOutdatedTensors_() {
1123 // for each clique, indicate whether the tensor stored into
1124 // _clique_ss_tensor_[clique] is the result of a combination. In this
1125 // case, it has been allocated by the combination and will need to be
1126 // deallocated if its clique has been invalidated
1127 NodeProperty< bool > ss_tensor_to_deallocate(_clique_tensors_.size());
1128 for (const auto& potset: _clique_tensors_) {
1129 ss_tensor_to_deallocate.insert(potset.first, (potset.second.size() > 1));
1130 }
1131
1132 // compute the set of CPTs that were projected due to hard evidence and
1133 // whose hard evidence have changed, so that they need a new projection.
1134 // By the way, remove these CPTs since they are no more needed
1135 // Here only the values of the hard evidence can have changed (else a
1136 // fully new join tree would have been computed).
1137 // Note also that we know that the CPTs still contain some variable(s) after
1138 // the projection (else they should be constants)
1139 NodeSet hard_nodes_changed(_hard_ev_nodes_.size());
1140 for (const auto node: _hard_ev_nodes_)
1141 if (_evidence_changes_.exists(node)) hard_nodes_changed.insert(node);
1142
1143 NodeSet nodes_with_projected_CPTs_changed;
1144 const auto& bn = this->BN();
1145 for (auto pot_iter = _node_to_hard_ev_projected_CPTs_.beginSafe();
1146 pot_iter != _node_to_hard_ev_projected_CPTs_.endSafe();
1147 ++pot_iter) {
1148 for (const auto var: bn.cpt(pot_iter.key()).variablesSequence()) {
1149 if (hard_nodes_changed.contains(bn.nodeId(*var))) {
1150 nodes_with_projected_CPTs_changed.insert(pot_iter.key());
1151 delete pot_iter.val();
1152 _clique_tensors_[_node_to_clique_[pot_iter.key()]].erase(pot_iter.val());
1153 _node_to_hard_ev_projected_CPTs_.erase(pot_iter);
1154 break;
1155 }
1156 }
1157 }
1158
1159
1160 // invalidate all the messages that are no more correct: start from each of
1161 // the nodes whose soft evidence has changed and perform a diffusion from
1162 // the clique into which the soft evidence has been entered, indicating that
1163 // the messages spreading from this clique are now invalid. At the same time,
1164 // if there were tensors created on the arcs over which the messages were
1165 // sent, remove them from memory. For all the cliques that received some
1166 // projected CPT that should now be changed, do the same.
1167 NodeSet invalidated_cliques(_JT_->size());
1168 for (const auto& pair: _evidence_changes_) {
1169 if (auto ptr_clique = _node_to_clique_.tryGet(pair.first)) {
1170 const auto clique = *ptr_clique;
1171 invalidated_cliques.insert(clique);
1172 for (const auto neighbor: _JT_->neighbours(clique)) {
1173 _diffuseMessageInvalidations_(clique, neighbor, invalidated_cliques);
1174 }
1175 }
1176 }
1177
1178 // now, add to the set of invalidated cliques those that contain projected
1179 // CPTs that were changed.
1180 for (const auto node: nodes_with_projected_CPTs_changed) {
1181 const auto clique = _node_to_clique_[node];
1182 invalidated_cliques.insert(clique);
1183 for (const auto neighbor: _JT_->neighbours(clique)) {
1184 _diffuseMessageInvalidations_(clique, neighbor, invalidated_cliques);
1185 }
1186 }
1187
1188 // now that we know the cliques whose set of tensors have been changed,
1189 // we can discard their corresponding Shafer-Shenoy tensor
1190 for (const auto clique: invalidated_cliques) {
1191 if (ss_tensor_to_deallocate[clique]) {
1192 delete _clique_ss_tensor_[clique];
1193 _clique_ss_tensor_[clique] = nullptr;
1194 }
1195 }
1196
1197
1198 // now we shall remove all the posteriors that belong to the
1199 // invalidated cliques. First, cope only with the nodes that did not
1200 // receive hard evidence since the other nodes do not belong to the
1201 // join tree
1202 for (auto iter = _target_posteriors_.beginSafe(); iter != _target_posteriors_.endSafe();
1203 ++iter) {
1204 if (_graph_.exists(iter.key())
1205 && (invalidated_cliques.exists(_node_to_clique_[iter.key()]))) {
1206 delete iter.val();
1207 _target_posteriors_.erase(iter);
1208 }
1209 }
1210
1211 // now cope with the nodes that received hard evidence
1212 for (auto iter = _target_posteriors_.beginSafe(); iter != _target_posteriors_.endSafe();
1213 ++iter) {
1214 if (hard_nodes_changed.contains(iter.key())) {
1215 delete iter.val();
1216 _target_posteriors_.erase(iter);
1217 }
1218 }
1219
1220 // finally, cope with joint targets. Notably, remove the joint posteriors whose
1221 // nodes have all received changed evidence
1222 for (auto iter = _joint_target_posteriors_.beginSafe();
1223 iter != _joint_target_posteriors_.endSafe();
1224 ++iter) {
1225 if (invalidated_cliques.exists(_joint_target_to_clique_[iter.key()])) {
1226 delete iter.val();
1227 _joint_target_posteriors_.erase(iter);
1228 } else {
1229 // check for sets in which all nodes have received evidence
1230 bool has_unevidenced_node = false;
1231 for (const auto node: iter.key()) {
1232 if (!hard_nodes_changed.exists(node)) {
1233 has_unevidenced_node = true;
1234 break;
1235 }
1236 }
1237 if (!has_unevidenced_node) {
1238 delete iter.val();
1239 _joint_target_posteriors_.erase(iter);
1240 }
1241 }
1242 }
1243
1244 // remove all the evidence that were entered into _node_to_soft_evidence_
1245 // and _clique_ss_tensor_ and add the new soft ones
1246 for (const auto& pot_pair: _node_to_soft_evidence_) {
1247 delete pot_pair.second;
1248 _clique_tensors_[_node_to_clique_[pot_pair.first]].erase(pot_pair.second);
1249 }
1250 _node_to_soft_evidence_.clear();
1251
1252 const auto& evidence = this->evidence();
1253 for (const auto node: this->softEvidenceNodes()) {
1254 auto ev_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(*evidence[node], false);
1255 _node_to_soft_evidence_.insert(node, ev_pot);
1256 _clique_tensors_[_node_to_clique_[node]].insert(ev_pot);
1257 }
1258
1259
1260 // Now add the projections of the CPTs due to newly changed hard evidence:
1261 // if we are performing updateOutdatedTensors_, this means that the
1262 // set of nodes that received hard evidence has not changed, only
1263 // their instantiations can have changed. So, if there is an entry
1264 // for node in _constants_, there will still be such an entry after
1265 // performing the new projections. Idem for _node_to_hard_ev_projected_CPTs_
1266 if (_use_schedules_) {
1267 Schedule schedule;
1268 for (const auto node: nodes_with_projected_CPTs_changed) {
1269 // perform the projection with a combine and project instance
1270 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node);
1271 const auto& variables = cpt.variablesSequence();
1272 _ScheduleMultiDimSet_ marg_cpt_set;
1273 const auto sched_cpt = schedule.insertTable< Tensor< GUM_SCALAR > >(cpt, false);
1274 marg_cpt_set.insert(sched_cpt);
1275
1276 gum::VariableSet hard_variables;
1277 for (const auto var: variables) {
1278 NodeId xnode = bn.nodeId(*var);
1279 if (_hard_ev_nodes_.exists(xnode)) {
1280 const auto pot = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[xnode], false);
1281 marg_cpt_set.insert(pot);
1282 hard_variables.insert(var);
1283 }
1284 }
1285
1286 // perform the combination of those tensors and their projection
1287 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
1288 _combination_op_,
1289 _projection_op_);
1290
1291 _ScheduleMultiDimSet_ new_cpt_list
1292 = combine_and_project.schedule(schedule, marg_cpt_set, hard_variables);
1293
1294 // there should be only one tensor in new_cpt_list
1295 if (new_cpt_list.size() != 1) {
1297 "the projection of a tensor containing " << "hard evidence is empty!");
1298 }
1299 auto projected_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1300 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(*new_cpt_list.begin()));
1301 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(projected_pot))
1302 ->makeResultsPersistent(true);
1303 _clique_tensors_[_node_to_clique_[node]].insert(projected_pot);
1304 _node_to_hard_ev_projected_CPTs_.insert(node, projected_pot);
1305 }
1306
1307 // here, the list of tensors stored in the invalidated cliques have
1308 // been updated. So, now, we can combine them to produce the Shafer-Shenoy
1309 // tensor stored into the clique
1310 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1311 for (const auto clique: invalidated_cliques) {
1312 const auto& potset = _clique_tensors_[clique];
1313
1314 if (potset.size() > 0) {
1315 // here, there will be an entry in _clique_ss_tensor_
1316 // If there is only one element in potset, this element shall be
1317 // stored into _clique_ss_tensor_, else all the elements of potset
1318 // shall be combined and their result shall be stored
1319 if (potset.size() == 1) {
1320 _clique_ss_tensor_[clique] = *(potset.cbegin());
1321 } else {
1322 for (const auto pot: potset)
1323 if (!schedule.existsScheduleMultiDim(pot->id()))
1324 schedule.emplaceScheduleMultiDim(*pot);
1325 auto joint = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1326 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1327 fast_combination.schedule(schedule, potset)));
1328 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(joint))
1329 ->makeResultsPersistent(true);
1330 _clique_ss_tensor_[clique] = joint;
1331 }
1332 }
1333 }
1334 this->scheduler().execute(schedule);
1335 } else {
1336 for (const auto node: nodes_with_projected_CPTs_changed) {
1337 // perform the projection with a combine and project instance
1338 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node);
1339 const auto& variables = cpt.variablesSequence();
1340 _TensorSet_ marg_cpt_set(1 + variables.size());
1341 marg_cpt_set.insert(&cpt);
1342
1343 gum::VariableSet hard_variables;
1344 for (const auto var: variables) {
1345 NodeId xnode = bn.nodeId(*var);
1346 if (_hard_ev_nodes_.exists(xnode)) {
1347 marg_cpt_set.insert(evidence[xnode]);
1348 hard_variables.insert(var);
1349 }
1350 }
1351
1352 // perform the combination of those tensors and their projection
1353 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
1354 _combination_op_,
1355 _projection_op_);
1356
1357 _TensorSet_ new_cpt_list = combine_and_project.execute(marg_cpt_set, hard_variables);
1358
1359 // there should be only one tensor in new_cpt_list
1360 if (new_cpt_list.size() != 1) {
1361 for (const auto pot: new_cpt_list)
1362 if (!marg_cpt_set.contains(pot)) delete pot;
1363
1365 "the projection of a tensor containing " << "hard evidence is empty!");
1366 }
1367 Tensor< GUM_SCALAR >* xprojected_pot
1368 = const_cast< Tensor< GUM_SCALAR >* >(*new_cpt_list.begin());
1369 auto projected_pot
1370 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*xprojected_pot));
1371 delete xprojected_pot;
1372 _clique_tensors_[_node_to_clique_[node]].insert(projected_pot);
1373 _node_to_hard_ev_projected_CPTs_.insert(node, projected_pot);
1374 }
1375
1376 // here, the list of tensors stored in the invalidated cliques have
1377 // been updated. So, now, we can combine them to produce the Shafer-Shenoy
1378 // tensor stored into the clique
1379 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1380 for (const auto clique: invalidated_cliques) {
1381 const auto& potset = _clique_tensors_[clique];
1382
1383 if (potset.size() > 0) {
1384 // here, there will be an entry in _clique_ss_tensor_
1385 // If there is only one element in potset, this element shall be
1386 // stored into _clique_ss_tensor_, else all the elements of potset
1387 // shall be combined and their result shall be stored
1388 if (potset.size() == 1) {
1389 _clique_ss_tensor_[clique] = *(potset.cbegin());
1390 } else {
1391 _TensorSet_ p_potset(potset.size());
1392 for (const auto pot: potset)
1393 p_potset.insert(&(
1394 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot)->multiDim()));
1395
1396 Tensor< GUM_SCALAR >* joint
1397 = const_cast< Tensor< GUM_SCALAR >* >(fast_combination.execute(p_potset));
1398 _clique_ss_tensor_[clique]
1399 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*joint));
1400 delete joint;
1401 }
1402 }
1403 }
1404 }
1405
1406 // update the constants
1407 const auto& hard_evidence = this->hardEvidence();
1408 for (auto& node_cst: _constants_) {
1409 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node_cst.first);
1410 const auto& variables = cpt.variablesSequence();
1411 Instantiation inst;
1412 for (const auto var: variables)
1413 inst << *var;
1414 for (const auto var: variables) {
1415 inst.chgVal(*var, hard_evidence[bn.nodeId(*var)]);
1416 }
1417 node_cst.second = cpt.get(inst);
1418 }
1419
1420 // indicate that all changes have been performed
1421 _evidence_changes_.clear();
1422 }
1423
1425 template < GUM_Numeric GUM_SCALAR >
1426 void ShaferShenoyInference< GUM_SCALAR >::_computeJoinTreeRoots_() {
1427 // get the set of cliques in which we can find the targets and joint_targets.
1428 // Due to hard evidence, the cliques related to a given target node
1429 // might not exist, hence the if checks
1430 NodeSet clique_targets;
1431 for (const auto node: this->targets()) {
1432 if (auto ptr_clique = _node_to_clique_.tryGet(node)) { clique_targets.insert(*ptr_clique); }
1433 }
1434 for (const auto& set: this->jointTargets()) {
1435 if (auto ptr_clique = _joint_target_to_clique_.tryGet(set)) {
1436 clique_targets.insert(*ptr_clique);
1437 }
1438 }
1439
1440 // put in a vector these cliques and their sizes
1441 std::vector< std::pair< NodeId, Size > > possible_roots(clique_targets.size());
1442 const auto& bn = this->BN();
1443 std::size_t i = 0;
1444 for (const auto clique_id: clique_targets) {
1445 const auto& clique = _JT_->clique(clique_id);
1446 Size dom_size = 1;
1447 for (const auto node: clique) {
1448 dom_size *= bn.variable(node).domainSize();
1449 }
1450 possible_roots[i] = std::pair< NodeId, Size >(clique_id, dom_size);
1451 ++i;
1452 }
1453
1454 // sort the cliques by increasing domain size
1455 std::sort(possible_roots.begin(),
1456 possible_roots.end(),
1457 [](const std::pair< NodeId, Size >& a, const std::pair< NodeId, Size >& b) -> bool {
1458 return a.second < b.second;
1459 });
1460
1461 // pick up the clique with the smallest size in each connected component
1462 NodeProperty< bool > marked = _JT_->nodesPropertyFromVal(false);
1463 std::function< void(NodeId, NodeId) > diffuse_marks
1464 = [&marked, &diffuse_marks, this](NodeId node, NodeId from) {
1465 if (!marked[node]) {
1466 marked[node] = true;
1467 for (const auto neigh: _JT_->neighbours(node))
1468 if ((neigh != from) && !marked[neigh]) diffuse_marks(neigh, node);
1469 }
1470 };
1471 _roots_.clear();
1472 for (const auto& xclique: possible_roots) {
1473 NodeId clique = xclique.first;
1474 if (!marked[clique]) {
1475 _roots_.insert(clique);
1476 diffuse_marks(clique, clique);
1477 }
1478 }
1479 }
1480
1481 // find the tensors d-connected to a set of variables
1482 template < GUM_Numeric GUM_SCALAR >
1483 void ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsGetAll_(
1484 Set< const IScheduleMultiDim* >& pot_list,
1485 gum::VariableSet& kept_vars) {}
1486
1487 // find the tensors d-connected to a set of variables
1488 template < GUM_Numeric GUM_SCALAR >
1489 void ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation_(
1490 Set< const IScheduleMultiDim* >& pot_list,
1491 gum::VariableSet& kept_vars) {
1492 // find the node ids of the kept variables
1493 NodeSet kept_ids(kept_vars.size());
1494 const auto& bn = this->BN();
1495 for (const auto var: kept_vars) {
1496 kept_ids.insert(bn.nodeId(*var));
1497 }
1498
1499 // determine the set of tensors d-connected with the kept variables
1500 NodeSet requisite_nodes;
1501 BayesBall::requisiteNodes(bn.internalDag(),
1502 kept_ids,
1503 this->hardEvidenceNodes(),
1504 this->softEvidenceNodes(),
1505 requisite_nodes);
1506 for (auto iter = pot_list.beginSafe(); iter != pot_list.endSafe(); ++iter) {
1507 const Sequence< const DiscreteVariable* >& vars = (*iter)->variablesSequence();
1508 bool found = false;
1509 for (const auto var: vars) {
1510 if (requisite_nodes.exists(bn.nodeId(*var))) {
1511 found = true;
1512 break;
1513 }
1514 }
1515
1516 if (!found) { pot_list.erase(iter); }
1517 }
1518 }
1519
1520 // find the tensors d-connected to a set of variables
1521 template < GUM_Numeric GUM_SCALAR >
1522 void ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation2_(
1523 Set< const IScheduleMultiDim* >& pot_list,
1524 gum::VariableSet& kept_vars) {
1525 // find the node ids of the kept variables
1526 NodeSet kept_ids(kept_vars.size());
1527 const auto& bn = this->BN();
1528 for (const auto var: kept_vars) {
1529 kept_ids.insert(bn.nodeId(*var));
1530 }
1531
1532 // determine the set of tensors d-connected with the kept variables
1533 BayesBall::relevantTensors(bn,
1534 kept_ids,
1535 this->hardEvidenceNodes(),
1536 this->softEvidenceNodes(),
1537 pot_list);
1538 }
1539
1540 // find the tensors d-connected to a set of variables
1541 template < GUM_Numeric GUM_SCALAR >
1542 void ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsWithdSeparation3_(
1543 Set< const IScheduleMultiDim* >& pot_list,
1544 gum::VariableSet& kept_vars) {
1545 // find the node ids of the kept variables
1546 NodeSet kept_ids(kept_vars.size());
1547 const auto& bn = this->BN();
1548 for (const auto var: kept_vars) {
1549 kept_ids.insert(bn.nodeId(*var));
1550 }
1551
1552 // determine the set of tensors d-connected with the kept variables
1553 dSeparationAlgorithm dsep;
1554 dsep.relevantTensors(bn,
1555 kept_ids,
1556 this->hardEvidenceNodes(),
1557 this->softEvidenceNodes(),
1558 pot_list);
1559 }
1560
1561 // find the tensors d-connected to a set of variables
1562 template < GUM_Numeric GUM_SCALAR >
1563 void ShaferShenoyInference< GUM_SCALAR >::_findRelevantTensorsXX_(
1564 Set< const IScheduleMultiDim* >& pot_list,
1565 gum::VariableSet& kept_vars) {
1566 switch (_find_relevant_tensor_type_) {
1567 case RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS :
1568 _findRelevantTensorsWithdSeparation2_(pot_list, kept_vars);
1569 break;
1570
1571 case RelevantTensorsFinderType::DSEP_BAYESBALL_NODES :
1572 _findRelevantTensorsWithdSeparation_(pot_list, kept_vars);
1573 break;
1574
1575 case RelevantTensorsFinderType::DSEP_KOLLER_FRIEDMAN_2009 :
1576 _findRelevantTensorsWithdSeparation3_(pot_list, kept_vars);
1577 break;
1578
1579 case RelevantTensorsFinderType::FIND_ALL :
1580 _findRelevantTensorsGetAll_(pot_list, kept_vars);
1581 break;
1582
1583 default : GUM_ERROR(FatalError, "not implemented yet")
1584 }
1585 }
1586
1587 // remove barren variables using schedules
1588 template < GUM_Numeric GUM_SCALAR >
1589 Set< const IScheduleMultiDim* >
1590 ShaferShenoyInference< GUM_SCALAR >::_removeBarrenVariables_(Schedule& schedule,
1591 _ScheduleMultiDimSet_& pot_list,
1592 gum::VariableSet& del_vars) {
1593 // remove from del_vars the variables that received some evidence:
1594 // only those that did not receive evidence can be barren variables
1595 gum::VariableSet the_del_vars = del_vars;
1596 for (auto iter = the_del_vars.beginSafe(); iter != the_del_vars.endSafe(); ++iter) {
1597 NodeId id = this->BN().nodeId(**iter);
1598 if (this->hardEvidenceNodes().exists(id) || this->softEvidenceNodes().exists(id)) {
1599 the_del_vars.erase(iter);
1600 }
1601 }
1602
1603 // assign to each random variable the set of tensors that contain it
1604 HashTable< const DiscreteVariable*, _ScheduleMultiDimSet_ > var2pots(the_del_vars.size());
1605 _ScheduleMultiDimSet_ empty_pot_set;
1606 for (const auto pot: pot_list) {
1607 const auto& vars = pot->variablesSequence();
1608 for (const auto var: vars) {
1609 if (the_del_vars.exists(var)) {
1610 if (!var2pots.exists(var)) { var2pots.insert(var, empty_pot_set); }
1611 var2pots[var].insert(pot);
1612 }
1613 }
1614 }
1615
1616 // each variable with only one tensor is necessarily a barren variable
1617 // assign to each tensor with barren nodes its set of barren variables
1618 HashTable< const IScheduleMultiDim*, gum::VariableSet > pot2barren_var;
1619 gum::VariableSet empty_var_set;
1620 for (const auto& elt: var2pots) {
1621 if (elt.second.size() == 1) { // here we have a barren variable
1622 const IScheduleMultiDim* pot = *(elt.second.begin());
1623 if (!pot2barren_var.exists(pot)) { pot2barren_var.insert(pot, empty_var_set); }
1624 pot2barren_var[pot].insert(elt.first); // insert the barren variable
1625 }
1626 }
1627
1628 // for each tensor with barren variables, marginalize them.
1629 // if the tensor has only barren variables, simply remove them from the
1630 // set of tensors, else just project the tensor
1631 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
1632 _ScheduleMultiDimSet_ projected_pots;
1633 for (const auto& elt: pot2barren_var) {
1634 // remove the current tensor from pot_list as, anyway, we will change it
1635 const IScheduleMultiDim* pot = elt.first;
1636 pot_list.erase(pot);
1637
1638 // check whether we need to add a projected new tensor or not (i.e.,
1639 // whether there exist non-barren variables or not)
1640 if (pot->variablesSequence().size() != elt.second.size()) {
1641 const IScheduleMultiDim* new_pot = projector.schedule(schedule, pot, elt.second);
1642 // here, there is no need to enforce that new_pot is persistent since,
1643 // if this is needed, the function that called _removeBarrenVariables_ will
1644 // do it
1645 pot_list.insert(new_pot);
1646 projected_pots.insert(new_pot);
1647 }
1648 }
1649
1650 return projected_pots;
1651 }
1652
1653 // remove barren variables directly without schedules
1654 template < GUM_Numeric GUM_SCALAR >
1655 Set< const Tensor< GUM_SCALAR >* >
1656 ShaferShenoyInference< GUM_SCALAR >::_removeBarrenVariables_(_TensorSet_& pot_list,
1657 gum::VariableSet& del_vars) {
1658 // remove from del_vars the variables that received some evidence:
1659 // only those that did not receive evidence can be barren variables
1660 gum::VariableSet the_del_vars = del_vars;
1661 for (auto iter = the_del_vars.beginSafe(); iter != the_del_vars.endSafe(); ++iter) {
1662 NodeId id = this->BN().nodeId(**iter);
1663 if (this->hardEvidenceNodes().exists(id) || this->softEvidenceNodes().exists(id)) {
1664 the_del_vars.erase(iter);
1665 }
1666 }
1667
1668 // assign to each random variable the set of tensors that contain it
1669 HashTable< const DiscreteVariable*, _TensorSet_ > var2pots(the_del_vars.size());
1670 _TensorSet_ empty_pot_set;
1671 for (const auto pot: pot_list) {
1672 const Sequence< const DiscreteVariable* >& vars = pot->variablesSequence();
1673 for (const auto var: vars) {
1674 if (the_del_vars.exists(var)) {
1675 if (!var2pots.exists(var)) { var2pots.insert(var, empty_pot_set); }
1676 var2pots[var].insert(pot);
1677 }
1678 }
1679 }
1680
1681 // each variable with only one tensor is a barren variable
1682 // assign to each tensor with barren nodes its set of barren variables
1683 HashTable< const Tensor< GUM_SCALAR >*, gum::VariableSet > pot2barren_var;
1684 gum::VariableSet empty_var_set;
1685 for (const auto& elt: var2pots) {
1686 if (elt.second.size() == 1) { // here we have a barren variable
1687 const Tensor< GUM_SCALAR >* pot = *(elt.second.begin());
1688 if (!pot2barren_var.exists(pot)) { pot2barren_var.insert(pot, empty_var_set); }
1689 pot2barren_var[pot].insert(elt.first); // insert the barren variable
1690 }
1691 }
1692
1693 // for each tensor with barren variables, marginalize them.
1694 // if the tensor has only barren variables, simply remove them from the
1695 // set of tensors, else just project the tensor
1696 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
1697 _TensorSet_ projected_pots;
1698 for (const auto& elt: pot2barren_var) {
1699 // remove the current tensor from pot_list as, anyway, we will change it
1700 const Tensor< GUM_SCALAR >* pot = elt.first;
1701 pot_list.erase(pot);
1702
1703 // check whether we need to add a projected new tensor or not (i.e.,
1704 // whether there exist non-barren variables or not)
1705 if (pot->variablesSequence().size() != elt.second.size()) {
1706 const Tensor< GUM_SCALAR >* new_pot = projector.execute(*pot, elt.second);
1707 pot_list.insert(new_pot);
1708 projected_pots.insert(new_pot);
1709 }
1710 }
1711
1712 return projected_pots;
1713 }
1714
1715 // performs the collect phase of Shafer-Shenoy using schedules
1716 template < GUM_Numeric GUM_SCALAR >
1717 void ShaferShenoyInference< GUM_SCALAR >::_collectMessage_(Schedule& schedule,
1718 NodeId id,
1719 NodeId from) {
1720 for (const auto other: _JT_->neighbours(id)) {
1721 if ((other != from) && !_messages_computed_[Arc(other, id)])
1722 _collectMessage_(schedule, other, id);
1723 }
1724
1725 if ((id != from) && !_messages_computed_[Arc(id, from)]) {
1726 _produceMessage_(schedule, id, from);
1727 }
1728 }
1729
1730 // performs the collect phase of Shafer-Shenoy without schedules
1731 template < GUM_Numeric GUM_SCALAR >
1732 void ShaferShenoyInference< GUM_SCALAR >::_collectMessage_(NodeId id, NodeId from) {
1733 for (const auto other: _JT_->neighbours(id)) {
1734 if ((other != from) && !_messages_computed_[Arc(other, id)]) _collectMessage_(other, id);
1735 }
1736
1737 if ((id != from) && !_messages_computed_[Arc(id, from)]) { _produceMessage_(id, from); }
1738 }
1739
1740 // remove variables del_vars from the list of tensors pot_list
1741 template < GUM_Numeric GUM_SCALAR >
1742 const IScheduleMultiDim* ShaferShenoyInference< GUM_SCALAR >::_marginalizeOut_(
1743 Schedule& schedule,
1744 Set< const IScheduleMultiDim* > pot_list,
1745 gum::VariableSet& del_vars,
1746 gum::VariableSet& kept_vars) {
1747 // use d-separation analysis to check which tensors shall be combined
1748 //_findRelevantTensorsXX_(pot_list, kept_vars);
1749
1750 // if pot list is empty, do nothing. This may happen when there are only barren variables
1751 if (pot_list.empty()) {
1752 return new ScheduleMultiDim< Tensor< GUM_SCALAR > >(Tensor< GUM_SCALAR >());
1753 }
1754
1755 // now, let's guarantee that all the tensors to be combined and projected
1756 // belong to the schedule
1757 for (const auto pot: pot_list) {
1758 if (!schedule.existsScheduleMultiDim(pot->id())) schedule.emplaceScheduleMultiDim(*pot);
1759 }
1760
1761 // remove the tensors corresponding to barren variables if we want
1762 // to exploit barren nodes
1763 _ScheduleMultiDimSet_ barren_projected_tensors;
1764 if (_barren_nodes_type_ == FindBarrenNodesType::FIND_BARREN_NODES) {
1765 barren_projected_tensors = _removeBarrenVariables_(schedule, pot_list, del_vars);
1766 }
1767
1768 // Combine and project the tensors
1769 _ScheduleMultiDimSet_ new_pot_list;
1770 if (pot_list.size() == 1) { // only one tensor, so just project it
1771 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
1772 auto xpot = projector.schedule(schedule, *(pot_list.begin()), del_vars);
1773 new_pot_list.insert(xpot);
1774 } else if (pot_list.size() > 1) {
1775 // create a combine and project operator that will perform the
1776 // marginalization
1777 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(_combination_op_,
1778 _projection_op_);
1779 new_pot_list = combine_and_project.schedule(schedule, pot_list, del_vars);
1780 }
1781
1782 // remove all the tensors that were created due to projections of
1783 // barren nodes and that are not part of the new_pot_list: these
1784 // tensors were just temporary tensors
1785 for (auto barren_pot: barren_projected_tensors) {
1786 if (!new_pot_list.exists(barren_pot))
1787 schedule.emplaceDeletion(
1788 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >& >(*barren_pot));
1789 }
1790
1791 // combine all the remaining tensors in order to create only one resulting tensor
1792 if (new_pot_list.empty())
1793 return new ScheduleMultiDim< Tensor< GUM_SCALAR > >(Tensor< GUM_SCALAR >());
1794 if (new_pot_list.size() == 1) return *(new_pot_list.begin());
1795 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1796 return fast_combination.schedule(schedule, new_pot_list);
1797 }
1798
1799 // remove variables del_vars from the list of tensors pot_list
1800 template < GUM_Numeric GUM_SCALAR >
1801 const IScheduleMultiDim* ShaferShenoyInference< GUM_SCALAR >::_marginalizeOut_(
1802 Set< const IScheduleMultiDim* >& pot_list,
1803 gum::VariableSet& del_vars,
1804 gum::VariableSet& kept_vars) {
1805 // if pot list is empty, do nothing. This may happen when there are only barren variables
1806 if (pot_list.empty()) {
1807 return new ScheduleMultiDim< Tensor< GUM_SCALAR > >(Tensor< GUM_SCALAR >());
1808 }
1809
1810 _TensorSet_ xpot_list(pot_list.size());
1811 for (auto pot: pot_list)
1812 xpot_list.insert(
1813 &(static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot)->multiDim()));
1814
1815 // use d-separation analysis to check which tensors shall be combined
1816 // _findRelevantTensorsXX_(pot_list, kept_vars);
1817
1818 // remove the tensors corresponding to barren variables if we want
1819 // to exploit barren nodes
1820 _TensorSet_ barren_projected_tensors;
1821 if (_barren_nodes_type_ == FindBarrenNodesType::FIND_BARREN_NODES) {
1822 barren_projected_tensors = _removeBarrenVariables_(xpot_list, del_vars);
1823 }
1824
1825 // Combine and project the remaining tensors
1826 _TensorSet_ xnew_pot_list;
1827 if (xpot_list.size() == 1) {
1828 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
1829 auto xpot = projector.execute(**(xpot_list.begin()), del_vars);
1830 xnew_pot_list.insert(xpot);
1831 } else if (xpot_list.size() > 1) {
1832 // create a combine and project operator that will perform the
1833 // marginalization
1834 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(_combination_op_,
1835 _projection_op_);
1836 xnew_pot_list = combine_and_project.execute(xpot_list, del_vars);
1837 }
1838
1839 // combine all the remaining tensors in order to create only one resulting tensor
1840 const Tensor< GUM_SCALAR >* xres_pot;
1841 ScheduleMultiDim< Tensor< GUM_SCALAR > >* res_pot;
1842 if (xnew_pot_list.size() == 1) {
1843 xres_pot = *(xnew_pot_list.begin());
1844 } else if (xnew_pot_list.size() > 1) {
1845 // combine all the tensors that resulted from the above combine and
1846 // projet execution
1847 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1848 xres_pot = fast_combination.execute(xnew_pot_list);
1849 for (const auto pot: xnew_pot_list) {
1850 if (!xpot_list.contains(pot) && (pot != xres_pot)) delete pot;
1851 }
1852 } else {
1853 xres_pot = new Tensor< GUM_SCALAR >();
1854 }
1855
1856 // transform xres_pot into a ScheduleMultiDim
1857 if (xpot_list.contains(xres_pot))
1858 res_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(*xres_pot, false);
1859 else {
1860 res_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(
1861 std::move(const_cast< Tensor< GUM_SCALAR >& >(*xres_pot)));
1862 delete xres_pot;
1863 }
1864
1865 // remove all the tensors that were created due to projections of
1866 // barren nodes and that are not part of the new_pot_list: these
1867 // tensors were just temporary tensors
1868 for (const auto barren_pot: barren_projected_tensors) {
1869 if (!xnew_pot_list.exists(barren_pot)) delete barren_pot;
1870 }
1871
1872 return res_pot;
1873 }
1874
1875 // creates the message sent by clique from_id to clique to_id
1876 template < GUM_Numeric GUM_SCALAR >
1877 void ShaferShenoyInference< GUM_SCALAR >::_produceMessage_(Schedule& schedule,
1878 NodeId from_id,
1879 NodeId to_id) {
1880 // get the tensors of the clique.
1881 _ScheduleMultiDimSet_ pot_list;
1882 if (_clique_ss_tensor_[from_id] != nullptr) pot_list.insert(_clique_ss_tensor_[from_id]);
1883
1884 // add the messages sent by adjacent nodes to from_id.
1885 for (const auto other_id: _JT_->neighbours(from_id)) {
1886 if (other_id != to_id) {
1887 const auto separator_pot = _separator_tensors_[Arc(other_id, from_id)];
1888 if (separator_pot != nullptr) pot_list.insert(separator_pot);
1889 }
1890 }
1891
1892 // get the set of variables that need be removed from the tensors
1893 const NodeSet& from_clique = _JT_->clique(from_id);
1894 const NodeSet& separator = _JT_->separator(from_id, to_id);
1895 gum::VariableSet del_vars(from_clique.size());
1896 gum::VariableSet kept_vars(separator.size());
1897 const auto& bn = this->BN();
1898
1899 for (const auto node: from_clique) {
1900 if (!separator.contains(node)) {
1901 del_vars.insert(&(bn.variable(node)));
1902 } else {
1903 kept_vars.insert(&(bn.variable(node)));
1904 }
1905 }
1906
1907 // pot_list now contains all the tensors to multiply and marginalize
1908 // => combine the messages
1909 const IScheduleMultiDim* new_pot = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
1910
1911 // keep track of the newly created tensor
1912 const Arc arc(from_id, to_id);
1913 if (!pot_list.exists(new_pot)) {
1914 if (!_arc_to_created_tensors_.exists(arc)) {
1915 _arc_to_created_tensors_.insert(arc, new_pot);
1916
1917 // do not forget to make the ScheduleMultiDim persistent
1918 auto op = schedule.scheduleMultiDimCreator(new_pot);
1919 if (op != nullptr) const_cast< ScheduleOperator* >(op)->makeResultsPersistent(true);
1920 }
1921 }
1922
1923 _separator_tensors_[arc] = new_pot;
1924 _messages_computed_[arc] = true;
1925 }
1926
1927 // creates the message sent by clique from_id to clique to_id
1928 template < GUM_Numeric GUM_SCALAR >
1929 void ShaferShenoyInference< GUM_SCALAR >::_produceMessage_(NodeId from_id, NodeId to_id) {
1930 // get the tensors of the clique.
1931 _ScheduleMultiDimSet_ pot_list;
1932 if (_clique_ss_tensor_[from_id] != nullptr) pot_list.insert(_clique_ss_tensor_[from_id]);
1933
1934 // add the messages sent by adjacent nodes to from_id.
1935 for (const auto other_id: _JT_->neighbours(from_id)) {
1936 if (other_id != to_id) {
1937 const auto separator_pot = _separator_tensors_[Arc(other_id, from_id)];
1938 if (separator_pot != nullptr) pot_list.insert(separator_pot);
1939 }
1940 }
1941
1942 // get the set of variables that need be removed from the tensors
1943 const NodeSet& from_clique = _JT_->clique(from_id);
1944 const NodeSet& separator = _JT_->separator(from_id, to_id);
1945 gum::VariableSet del_vars(from_clique.size());
1946 gum::VariableSet kept_vars(separator.size());
1947 const auto& bn = this->BN();
1948
1949 for (const auto node: from_clique) {
1950 if (!separator.contains(node)) {
1951 del_vars.insert(&(bn.variable(node)));
1952 } else {
1953 kept_vars.insert(&(bn.variable(node)));
1954 }
1955 }
1956
1957 // pot_list now contains all the tensors to multiply and marginalize
1958 // => combine the messages
1959 const IScheduleMultiDim* new_pot = _marginalizeOut_(pot_list, del_vars, kept_vars);
1960
1961 // keep track of the newly created tensor
1962 const Arc arc(from_id, to_id);
1963 if (!pot_list.exists(new_pot)) {
1964 if (!_arc_to_created_tensors_.exists(arc)) { _arc_to_created_tensors_.insert(arc, new_pot); }
1965 }
1966
1967 _separator_tensors_[arc] = new_pot;
1968 _messages_computed_[arc] = true;
1969 }
1970
1971 // performs a whole inference
1972 template < GUM_Numeric GUM_SCALAR >
1973 void ShaferShenoyInference< GUM_SCALAR >::makeInference_() {
1974 if (_use_schedules_) {
1975 Schedule schedule;
1976
1977 // collect messages for all single targets
1978 for (const auto node: this->targets()) {
1979 // perform only collects in the join tree for nodes that have
1980 // not received hard evidence (those that received hard evidence were
1981 // not included into the join tree for speed-up reasons)
1982 if (_graph_.exists(node)) {
1983 _collectMessage_(schedule, _node_to_clique_[node], _node_to_clique_[node]);
1984 }
1985 }
1986
1987 // collect messages for all set targets
1988 // by parsing _joint_target_to_clique_, we ensure that the cliques that
1989 // are referenced belong to the join tree (even if some of the nodes in
1990 // their associated joint_target do not belong to _graph_)
1991 for (const auto& set: _joint_target_to_clique_)
1992 _collectMessage_(schedule, set.second, set.second);
1993
1994 // really perform the computations
1995 this->scheduler().execute(schedule);
1996 } else {
1997 // collect messages for all single targets
1998 for (const auto node: this->targets()) {
1999 // perform only collects in the join tree for nodes that have
2000 // not received hard evidence (those that received hard evidence were
2001 // not included into the join tree for speed-up reasons)
2002 if (_graph_.exists(node)) {
2003 _collectMessage_(_node_to_clique_[node], _node_to_clique_[node]);
2004 }
2005 }
2006
2007 // collect messages for all set targets
2008 // by parsing _joint_target_to_clique_, we ensure that the cliques that
2009 // are referenced belong to the join tree (even if some of the nodes in
2010 // their associated joint_target do not belong to _graph_)
2011 for (const auto& set: _joint_target_to_clique_)
2012 _collectMessage_(set.second, set.second);
2013 }
2014 }
2015
2017 template < GUM_Numeric GUM_SCALAR >
2018 Tensor< GUM_SCALAR >*
2019 ShaferShenoyInference< GUM_SCALAR >::unnormalizedJointPosterior_(NodeId id) {
2020 if (_use_schedules_) {
2021 Schedule schedule;
2022 return _unnormalizedJointPosterior_(schedule, id);
2023 } else {
2024 return _unnormalizedJointPosterior_(id);
2025 }
2026 }
2027
2029 template < GUM_Numeric GUM_SCALAR >
2030 Tensor< GUM_SCALAR >*
2031 ShaferShenoyInference< GUM_SCALAR >::_unnormalizedJointPosterior_(Schedule& schedule,
2032 NodeId id) {
2033 const auto& bn = this->BN();
2034
2035 // hard evidence do not belong to the join tree
2036 // # TODO: check for sets of inconsistent hard evidence
2037 if (this->hardEvidenceNodes().contains(id)) {
2038 return new Tensor< GUM_SCALAR >(*(this->evidence()[id]));
2039 }
2040
2041 auto& scheduler = this->scheduler();
2042
2043 // if we still need to perform some inference task, do it (this should
2044 // already have been done by makeInference_)
2045 const NodeId clique_of_id = _node_to_clique_[id];
2046 _collectMessage_(schedule, clique_of_id, clique_of_id);
2047
2048 // now we just need to create the product of the tensors of the clique
2049 // containing id with the messages received by this clique and
2050 // marginalize out all variables except id
2051 _ScheduleMultiDimSet_ pot_list;
2052 if (_clique_ss_tensor_[clique_of_id] != nullptr)
2053 pot_list.insert(_clique_ss_tensor_[clique_of_id]);
2054
2055 // add the messages sent by adjacent nodes to targetClique
2056 for (const auto other: _JT_->neighbours(clique_of_id))
2057 pot_list.insert(_separator_tensors_[Arc(other, clique_of_id)]);
2058
2059 // get the set of variables that need be removed from the tensors
2060 const NodeSet& nodes = _JT_->clique(clique_of_id);
2061 gum::VariableSet kept_vars{&(bn.variable(id))};
2062 gum::VariableSet del_vars(nodes.size());
2063 for (const auto node: nodes) {
2064 if (node != id) del_vars.insert(&(bn.variable(node)));
2065 }
2066
2067 // pot_list now contains all the tensors to multiply and marginalize
2068 // => combine the messages
2069 auto resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2070 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2071 _marginalizeOut_(schedule, pot_list, del_vars, kept_vars)));
2072 Tensor< GUM_SCALAR >* joint = nullptr;
2073
2074 scheduler.execute(schedule);
2075
2076 // if pot already existed, create a copy, so that we can put it into
2077 // the _target_posteriors_ property
2078 if (pot_list.exists(resulting_pot)) {
2079 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
2080 } else {
2081 joint = resulting_pot->exportMultiDim();
2082 }
2083
2084 // check that the joint posterior is different from a 0 vector: this would
2085 // indicate that some hard evidence are not compatible (their joint
2086 // probability is equal to 0)
2087 bool nonzero_found = false;
2088 for (Instantiation inst(*joint); !inst.end(); ++inst) {
2089 if (joint->get(inst)) {
2090 nonzero_found = true;
2091 break;
2092 }
2093 }
2094 if (!nonzero_found) {
2095 // remove joint from memory to avoid memory leaks
2096 delete joint;
2098 "some evidence entered into the Bayes "
2099 "net are incompatible (their joint proba = 0)");
2100 }
2101 return joint;
2102 }
2103
2105 template < GUM_Numeric GUM_SCALAR >
2106 Tensor< GUM_SCALAR >*
2107 ShaferShenoyInference< GUM_SCALAR >::_unnormalizedJointPosterior_(NodeId id) {
2108 const auto& bn = this->BN();
2109
2110 // hard evidence do not belong to the join tree
2111 // # TODO: check for sets of inconsistent hard evidence
2112 if (this->hardEvidenceNodes().contains(id)) {
2113 return new Tensor< GUM_SCALAR >(*(this->evidence()[id]));
2114 }
2115
2116 // if we still need to perform some inference task, do it (this should
2117 // already have been done by makeInference_)
2118 NodeId clique_of_id = _node_to_clique_[id];
2119 _collectMessage_(clique_of_id, clique_of_id);
2120
2121 // now we just need to create the product of the tensors of the clique
2122 // containing id with the messages received by this clique and
2123 // marginalize out all variables except id
2124 _ScheduleMultiDimSet_ pot_list;
2125 if (_clique_ss_tensor_[clique_of_id] != nullptr)
2126 pot_list.insert(_clique_ss_tensor_[clique_of_id]);
2127
2128 // add the messages sent by adjacent nodes to targetClique
2129 for (const auto other: _JT_->neighbours(clique_of_id))
2130 pot_list.insert(_separator_tensors_[Arc(other, clique_of_id)]);
2131
2132 // get the set of variables that need be removed from the tensors
2133 const NodeSet& nodes = _JT_->clique(clique_of_id);
2134 gum::VariableSet kept_vars{&(bn.variable(id))};
2135 gum::VariableSet del_vars(nodes.size());
2136 for (const auto node: nodes) {
2137 if (node != id) del_vars.insert(&(bn.variable(node)));
2138 }
2139
2140 // pot_list now contains all the tensors to multiply and marginalize
2141 // => combine the messages
2142 auto resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2143 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2144 _marginalizeOut_(pot_list, del_vars, kept_vars)));
2145 Tensor< GUM_SCALAR >* joint = nullptr;
2146
2147 // if pot already existed, create a copy, so that we can put it into
2148 // the _target_posteriors_ property
2149 if (pot_list.exists(resulting_pot)) {
2150 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
2151 } else {
2152 joint = resulting_pot->exportMultiDim();
2153 delete resulting_pot;
2154 }
2155
2156 // check that the joint posterior is different from a 0 vector: this would
2157 // indicate that some hard evidence are not compatible (their joint
2158 // probability is equal to 0)
2159 bool nonzero_found = false;
2160 for (Instantiation inst(*joint); !inst.end(); ++inst) {
2161 if (joint->get(inst)) {
2162 nonzero_found = true;
2163 break;
2164 }
2165 }
2166 if (!nonzero_found) {
2167 // remove joint from memory to avoid memory leaks
2168 delete joint;
2170 "some evidence entered into the Bayes "
2171 "net are incompatible (their joint proba = 0)");
2172 }
2173 return joint;
2174 }
2175
2177 template < GUM_Numeric GUM_SCALAR >
2178 const Tensor< GUM_SCALAR >& ShaferShenoyInference< GUM_SCALAR >::posterior_(NodeId id) {
2179 // check if we have already computed the posterior
2180 if (_target_posteriors_.exists(id)) { return *(_target_posteriors_[id]); }
2181
2182 // compute the joint posterior and normalize
2183 auto joint = unnormalizedJointPosterior_(id);
2184 if (joint->sum() != 1) // hard test for ReadOnly CPT (as aggregator)
2185 joint->normalize();
2186 _target_posteriors_.insert(id, joint);
2187
2188 return *joint;
2189 }
2190
2192 template < GUM_Numeric GUM_SCALAR >
2193 Tensor< GUM_SCALAR >*
2194 ShaferShenoyInference< GUM_SCALAR >::unnormalizedJointPosterior_(const NodeSet& set) {
2195 if (_use_schedules_) {
2196 Schedule schedule;
2197 return _unnormalizedJointPosterior_(schedule, set);
2198 } else {
2199 return _unnormalizedJointPosterior_(set);
2200 }
2201 }
2202
2204 template < GUM_Numeric GUM_SCALAR >
2205 Tensor< GUM_SCALAR >*
2206 ShaferShenoyInference< GUM_SCALAR >::_unnormalizedJointPosterior_(Schedule& schedule,
2207 const NodeSet& set) {
2208 // hard evidence do not belong to the join tree, so extract the nodes
2209 // from targets that are not hard evidence
2210 NodeSet targets = set, hard_ev_nodes;
2211 for (const auto node: this->hardEvidenceNodes()) {
2212 if (targets.contains(node)) {
2213 targets.erase(node);
2214 hard_ev_nodes.insert(node);
2215 }
2216 }
2217
2218 auto& scheduler = this->scheduler();
2219
2220 // if all the nodes have received hard evidence, then compute the
2221 // joint posterior directly by multiplying the hard evidence tensors
2222 const auto& evidence = this->evidence();
2223 if (targets.empty()) {
2224 if (set.size() == 1) {
2225 return new Tensor< GUM_SCALAR >(*evidence[*set.begin()]);
2226 } else {
2227 _ScheduleMultiDimSet_ pot_list;
2228 for (const auto node: set) {
2229 auto new_pot_ev = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[node], false);
2230 pot_list.insert(new_pot_ev);
2231 }
2232
2233 // combine all the tensors of the nodes in set
2234 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
2235 const IScheduleMultiDim* pot = fast_combination.schedule(schedule, pot_list);
2236 auto schedule_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2237 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot));
2238 scheduler.execute(schedule);
2239 auto result = schedule_pot->exportMultiDim();
2240
2241 return result;
2242 }
2243 }
2244
2245
2246 // if we still need to perform some inference task, do it: so, first,
2247 // determine the clique on which we should perform collect to compute
2248 // the unnormalized joint posterior of a set of nodes containing "targets"
2249 NodeId clique_of_set;
2250 if (auto ptr_clique = _joint_target_to_clique_.tryGet(set)) {
2251 clique_of_set = *ptr_clique;
2252 } else {
2253 // here, the precise set of targets does not belong to the set of targets
2254 // defined by the user. So we will try to find a clique in the junction
2255 // tree that contains "targets":
2256
2257 // 1/ we should check that all the nodes belong to the join tree
2258 for (const auto node: targets) {
2259 if (!_graph_.exists(node)) {
2261 "The variable " << this->BN().variable(node).name() << "(" << node
2262 << ") does not belong to this optimized inference.")
2263 }
2264 }
2265
2266 // 2/ the clique created by the first eliminated node among target is the
2267 // one we are looking for
2268 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
2269
2270 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
2271 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
2272 elim_order.insert(JT_elim_order[i], (int)i);
2273 NodeId first_eliminated_node = *(targets.begin());
2274 int elim_number = elim_order[first_eliminated_node];
2275 for (const auto node: targets) {
2276 if (elim_order[node] < elim_number) {
2277 elim_number = elim_order[node];
2278 first_eliminated_node = node;
2279 }
2280 }
2281
2282 clique_of_set = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
2283
2284
2285 // 3/ check that clique_of_set contains the all the nodes in the target
2286 const NodeSet& clique_nodes = _JT_->clique(clique_of_set);
2287 for (const auto node: targets) {
2288 if (!clique_nodes.contains(node)) {
2290 this->BN().names(set) << "(" << set << ")"
2291 << " is not addressable in this optimized inference.")
2292 }
2293 }
2294
2295 // add the discovered clique to _joint_target_to_clique_
2296 _joint_target_to_clique_.insert(set, clique_of_set);
2297 }
2298
2299 // now perform a collect on the clique
2300 _collectMessage_(schedule, clique_of_set, clique_of_set);
2301
2302 // now we just need to create the product of the tensors of the clique
2303 // containing set with the messages received by this clique and
2304 // marginalize out all variables except set
2305 _ScheduleMultiDimSet_ pot_list;
2306 if (_clique_ss_tensor_[clique_of_set] != nullptr) {
2307 auto pot = _clique_ss_tensor_[clique_of_set];
2308 if (!schedule.existsScheduleMultiDim(pot->id())) schedule.emplaceScheduleMultiDim(*pot);
2309 pot_list.insert(_clique_ss_tensor_[clique_of_set]);
2310 }
2311
2312 // add the messages sent by adjacent nodes to targetClique
2313 for (const auto other: _JT_->neighbours(clique_of_set)) {
2314 const auto pot = _separator_tensors_[Arc(other, clique_of_set)];
2315 if (pot != nullptr) pot_list.insert(pot);
2316 }
2317
2318
2319 // get the set of variables that need be removed from the tensors
2320 const NodeSet& nodes = _JT_->clique(clique_of_set);
2321 gum::VariableSet del_vars(nodes.size());
2322 gum::VariableSet kept_vars(targets.size());
2323 const auto& bn = this->BN();
2324 for (const auto node: nodes) {
2325 if (!targets.contains(node)) {
2326 del_vars.insert(&(bn.variable(node)));
2327 } else {
2328 kept_vars.insert(&(bn.variable(node)));
2329 }
2330 }
2331
2332 // pot_list now contains all the tensors to multiply and marginalize
2333 // => combine the messages
2334 const IScheduleMultiDim* new_pot = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
2335 scheduler.execute(schedule);
2336 ScheduleMultiDim< Tensor< GUM_SCALAR > >* resulting_pot
2337 = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2338 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(new_pot));
2339
2340 // if pot already existed, create a copy, so that we can put it into
2341 // the _target_posteriors_ property
2342 Tensor< GUM_SCALAR >* joint = nullptr;
2343 if (pot_list.exists(resulting_pot)) {
2344 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
2345 } else {
2346 joint = resulting_pot->exportMultiDim();
2347 }
2348
2349 // check that the joint posterior is different from a 0 vector: this would
2350 // indicate that some hard evidence are not compatible
2351 bool nonzero_found = false;
2352 for (Instantiation inst(*joint); !inst.end(); ++inst) {
2353 if ((*joint)[inst]) {
2354 nonzero_found = true;
2355 break;
2356 }
2357 }
2358 if (!nonzero_found) {
2359 // remove joint from memory to avoid memory leaks
2360 delete joint;
2362 "some evidence entered into the Bayes "
2363 "net are incompatible (their joint proba = 0)");
2364 }
2365
2366 // if there exists some nodes in the set of targets that received hard evidence,
2367 // then the joint Tensor needs be multiplied by the evidence tensors of
2368 // these nodes
2369 if (!hard_ev_nodes.empty()) {
2370 _TensorSet_ pot_list;
2371 pot_list.insert(joint);
2372 const auto& hard_evidence = this->evidence();
2373 for (const auto node: hard_ev_nodes)
2374 pot_list.insert(hard_evidence[node]);
2375 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > combine(_combination_op_);
2376 Tensor< GUM_SCALAR >* new_joint = combine.execute(pot_list);
2377 delete joint;
2378 joint = new_joint;
2379 }
2380
2381 return joint;
2382 }
2383
2385 template < GUM_Numeric GUM_SCALAR >
2386 Tensor< GUM_SCALAR >*
2387 ShaferShenoyInference< GUM_SCALAR >::_unnormalizedJointPosterior_(const NodeSet& set) {
2388 // hard evidence do not belong to the join tree, so extract the nodes
2389 // from targets that are not hard evidence
2390 NodeSet targets = set, hard_ev_nodes;
2391 for (const auto node: this->hardEvidenceNodes()) {
2392 if (targets.contains(node)) {
2393 targets.erase(node);
2394 hard_ev_nodes.insert(node);
2395 }
2396 }
2397
2398 // if all the nodes have received hard evidence, then compute the
2399 // joint posterior directly by multiplying the hard evidence tensors
2400 const auto& evidence = this->evidence();
2401 if (targets.empty()) {
2402 if (set.size() == 1) {
2403 return new Tensor< GUM_SCALAR >(*evidence[*set.begin()]);
2404 } else {
2405 _TensorSet_ pot_list;
2406 for (const auto node: set) {
2407 pot_list.insert(evidence[node]);
2408 }
2409
2410 // combine all the tensors of the nodes in set
2411 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
2412 const Tensor< GUM_SCALAR >* pot = fast_combination.execute(pot_list);
2413
2414 return const_cast< Tensor< GUM_SCALAR >* >(pot);
2415 }
2416 }
2417
2418
2419 // if we still need to perform some inference task, do it: so, first,
2420 // determine the clique on which we should perform collect to compute
2421 // the unnormalized joint posterior of a set of nodes containing "targets"
2422 NodeId clique_of_set;
2423 if (auto ptr_clique = _joint_target_to_clique_.tryGet(set)) {
2424 clique_of_set = *ptr_clique;
2425 } else {
2426 // here, the precise set of targets does not belong to the set of targets
2427 // defined by the user. So we will try to find a clique in the junction
2428 // tree that contains "targets":
2429
2430 // 1/ we should check that all the nodes belong to the join tree
2431 for (const auto node: targets) {
2432 if (!_graph_.exists(node)) {
2434 node << " cannot be a query in the optimized inference (w.r.t the declared "
2435 "targets/evidence)")
2436 }
2437 }
2438
2439 // 2/ the clique created by the first eliminated node among target is the
2440 // one we are looking for
2441 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
2442
2443 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
2444 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
2445 elim_order.insert(JT_elim_order[i], (int)i);
2446 NodeId first_eliminated_node = *(targets.begin());
2447 int elim_number = elim_order[first_eliminated_node];
2448 for (const auto node: targets) {
2449 if (elim_order[node] < elim_number) {
2450 elim_number = elim_order[node];
2451 first_eliminated_node = node;
2452 }
2453 }
2454
2455 clique_of_set = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
2456
2457 // 3/ check that clique_of_set contains the all the nodes in the target
2458 const NodeSet& clique_nodes = _JT_->clique(clique_of_set);
2459 for (const auto node: targets) {
2460 if (!clique_nodes.contains(node)) {
2462 this->BN().names(set) << "(" << set << ")"
2463 << " is not addressable in this optimized inference.")
2464 }
2465 }
2466
2467 // add the discovered clique to _joint_target_to_clique_
2468 _joint_target_to_clique_.insert(set, clique_of_set);
2469 }
2470
2471 // now perform a collect on the clique
2472 _collectMessage_(clique_of_set, clique_of_set);
2473
2474 // now we just need to create the product of the tensors of the clique
2475 // containing set with the messages received by this clique and
2476 // marginalize out all variables except set
2477 _ScheduleMultiDimSet_ pot_list;
2478 if (_clique_ss_tensor_[clique_of_set] != nullptr) {
2479 auto pot = _clique_ss_tensor_[clique_of_set];
2480 if (pot != nullptr) pot_list.insert(_clique_ss_tensor_[clique_of_set]);
2481 }
2482
2483 // add the messages sent by adjacent nodes to targetClique
2484 for (const auto other: _JT_->neighbours(clique_of_set)) {
2485 const auto pot = _separator_tensors_[Arc(other, clique_of_set)];
2486 if (pot != nullptr) pot_list.insert(pot);
2487 }
2488
2489 // get the set of variables that need be removed from the tensors
2490 const NodeSet& nodes = _JT_->clique(clique_of_set);
2491 gum::VariableSet del_vars(nodes.size());
2492 gum::VariableSet kept_vars(targets.size());
2493 const auto& bn = this->BN();
2494 for (const auto node: nodes) {
2495 if (!targets.contains(node)) {
2496 del_vars.insert(&(bn.variable(node)));
2497 } else {
2498 kept_vars.insert(&(bn.variable(node)));
2499 }
2500 }
2501
2502 // pot_list now contains all the tensors to multiply and marginalize
2503 // => combine the messages
2504 const IScheduleMultiDim* new_pot = _marginalizeOut_(pot_list, del_vars, kept_vars);
2505 ScheduleMultiDim< Tensor< GUM_SCALAR > >* resulting_pot
2506 = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
2507 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(new_pot));
2508
2509 // if pot already existed, create a copy, so that we can put it into
2510 // the _target_posteriors_ property
2511 Tensor< GUM_SCALAR >* joint = nullptr;
2512 if (pot_list.exists(resulting_pot)) {
2513 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
2514 } else {
2515 joint = resulting_pot->exportMultiDim();
2516 delete new_pot;
2517 }
2518
2519 // check that the joint posterior is different from a 0 vector: this would
2520 // indicate that some hard evidence are not compatible
2521 bool nonzero_found = false;
2522 for (Instantiation inst(*joint); !inst.end(); ++inst) {
2523 if ((*joint)[inst]) {
2524 nonzero_found = true;
2525 break;
2526 }
2527 }
2528 if (!nonzero_found) {
2529 // remove joint from memory to avoid memory leaks
2530 delete joint;
2532 "some evidence entered into the Bayes "
2533 "net are incompatible (their joint proba = 0)");
2534 }
2535
2536 // if there exists some nodes in the set of targets that received hard evidence,
2537 // then the joint Tensor needs be multiplied by the evidence tensors of
2538 // these nodes
2539 if (!hard_ev_nodes.empty()) {
2540 _TensorSet_ pot_list;
2541 pot_list.insert(joint);
2542 const auto& hard_evidence = this->evidence();
2543 for (const auto node: hard_ev_nodes)
2544 pot_list.insert(hard_evidence[node]);
2545 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > combine(_combination_op_);
2546 Tensor< GUM_SCALAR >* new_joint = combine.execute(pot_list);
2547 delete joint;
2548 joint = new_joint;
2549 }
2550
2551 return joint;
2552 }
2553
2555 template < GUM_Numeric GUM_SCALAR >
2556 const Tensor< GUM_SCALAR >&
2557 ShaferShenoyInference< GUM_SCALAR >::jointPosterior_(const NodeSet& set) {
2558 // check if we have already computed the posterior
2559 if (_joint_target_posteriors_.exists(set)) { return *(_joint_target_posteriors_[set]); }
2560
2561 // compute the joint posterior and normalize
2562 auto joint = unnormalizedJointPosterior_(set);
2563 joint->normalize();
2564 _joint_target_posteriors_.insert(set, joint);
2565
2566 return *joint;
2567 }
2568
2570 template < GUM_Numeric GUM_SCALAR >
2571 const Tensor< GUM_SCALAR >&
2572 ShaferShenoyInference< GUM_SCALAR >::jointPosterior_(const NodeSet& wanted_target,
2573 const NodeSet& declared_target) {
2574 // check if we have already computed the posterior of wanted_target
2575 if (_joint_target_posteriors_.exists(wanted_target))
2576 return *(_joint_target_posteriors_[wanted_target]);
2577
2578 // here, we will have to compute the posterior of declared_target and
2579 // marginalize out all the variables that do not belong to wanted_target
2580
2581 // check if we have already computed the posterior of declared_target
2582 if (!_joint_target_posteriors_.exists(declared_target)) { jointPosterior_(declared_target); }
2583
2584 // marginalize out all the variables that do not belong to wanted_target
2585 const auto& bn = this->BN();
2586 gum::VariableSet del_vars;
2587 for (const auto node: declared_target)
2588 if (!wanted_target.contains(node)) del_vars.insert(&(bn.variable(node)));
2589 auto pot
2590 = new Tensor< GUM_SCALAR >(_joint_target_posteriors_[declared_target]->sumOut(del_vars));
2591
2592 // save the result into the cache
2593 _joint_target_posteriors_.insert(wanted_target, pot);
2594
2595 return *pot;
2596 }
2597
2598 template < GUM_Numeric GUM_SCALAR >
2599 GUM_SCALAR ShaferShenoyInference< GUM_SCALAR >::evidenceProbability() {
2600 // here, we should check that _find_relevant_tensor_type_ is equal to
2601 // FIND_ALL. Otherwise, the computations could be wrong.
2602 RelevantTensorsFinderType old_relevant_type = _find_relevant_tensor_type_;
2603
2604 // if the relevant tensors finder is not equal to FIND_ALL, all the
2605 // current computations may lead to incorrect results, so we shall
2606 // discard them
2607 if (old_relevant_type != RelevantTensorsFinderType::FIND_ALL) {
2608 _find_relevant_tensor_type_ = RelevantTensorsFinderType::FIND_ALL;
2609 _is_new_jt_needed_ = true;
2610 this->setOutdatedStructureState_();
2611 }
2612
2613 // perform inference in each connected component
2614 this->makeInference();
2615
2616 // for each connected component, select a variable X and compute the
2617 // joint probability of X and evidence e. Then marginalize-out X to get
2618 // p(e) in this connected component. Finally, multiply all the p(e) that
2619 // we got and the elements in _constants_. The result is the probability
2620 // of evidence
2621
2622 GUM_SCALAR prob_ev = 1;
2623 for (const auto root: _roots_) {
2624 // get a node in the clique
2625 NodeId node = *(_JT_->clique(root).begin());
2626 Tensor< GUM_SCALAR >* tmp = unnormalizedJointPosterior_(node);
2627 prob_ev *= tmp->sum();
2628 delete tmp;
2629 }
2630
2631 for (const auto& projected_cpt: _constants_)
2632 prob_ev *= projected_cpt.second;
2633
2634 // put back the relevant tensor type selected by the user
2635 _find_relevant_tensor_type_ = old_relevant_type;
2636
2637 return prob_ev;
2638 }
2639
2640 template < GUM_Numeric GUM_SCALAR >
2641 Tensor< GUM_SCALAR > SSNewmultiTensor(const Tensor< GUM_SCALAR >& t1,
2642 const Tensor< GUM_SCALAR >& t2) {
2643 return t1 * t2;
2644 }
2645
2646 template < GUM_Numeric GUM_SCALAR >
2647 Tensor< GUM_SCALAR > SSNewprojTensor(const Tensor< GUM_SCALAR >& t1,
2648 const gum::VariableSet& del_vars) {
2649 return t1.sumOut(del_vars);
2650 }
2651
2652 template < GUM_Numeric GUM_SCALAR >
2653 void ShaferShenoyInference< GUM_SCALAR >::onStateChanged_() {}
2654
2655} /* namespace gum */
2656
2657#endif // DOXYGEN_SHOULD_SKIP_THIS
The BayesBall algorithm (as described by Schachter).
Implementation of Shafer-Shenoy's algorithm for inference in Bayesian networks.
Detect barren nodes for inference in Bayesian networks.
An algorithm for converting a join tree into a binary join tree.
Exception : a similar element already exists.
<agrum/BN/inference/evidenceInference.h>
Exception : fatal (unknown ?) error.
Class representing the minimal interface for Bayesian network with no numerical data.
Definition IBayesNet.h:75
Exception : several evidence are incompatible together (proba=0).
Exception: at least one argument passed to a function is not what was expected.
<agrum/BN/inference/jointTargetedInference.h>
bool exists(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:504
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:510
iterator_safe beginSafe() const
The usual safe begin iterator to parse the set.
Definition set_tpl.h:385
void erase(const Key &k)
Erases an element from the set.
Definition set_tpl.h:553
Size size() const noexcept
Returns the number of elements in the set.
Definition set_tpl.h:607
static const iterator_safe & endSafe() noexcept
The usual safe end iterator to parse the set.
Definition set_tpl.h:397
ShaferShenoyInference(const IBayesNet< GUM_SCALAR > *BN, RelevantTensorsFinderType=RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS, FindBarrenNodesType barren_type=FindBarrenNodesType::FIND_BARREN_NODES, bool use_binary_join_tree=true)
default constructor
Exception : a looked-for element could not be found.
d-separation analysis (as described in Koller & Friedman 2009)
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
bool contains(std::string_view s, std::string_view needle)
true if needle in s
Header files of gum::Instantiation.
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
FindBarrenNodesType
type of algorithm to determine barren nodes
Set< const DiscreteVariable * > VariableSet
CliqueGraph JoinTree
a join tree is a clique graph satisfying the running intersection property (but some cliques may be i...
CliqueGraph JunctionTree
a junction tree is a clique graph satisfying the running intersection property and such that no cliqu...
RelevantTensorsFinderType
type of algorithm for determining the relevant tensors for combinations using some d-separation analy...