aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
ShaferShenoyMRFInference_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/MRF/inference/ShaferShenoyMRFInference.h> // to ease IDE parser
53#ifndef DOXYGEN_SHOULD_SKIP_THIS
54# include <algorithm>
55
61
62namespace gum {
63 // default constructor
64 template < GUM_Numeric GUM_SCALAR >
67 bool use_binary_join_tree) :
68 JointTargetedMRFInference< GUM_SCALAR >(MN), EvidenceMRFInference< GUM_SCALAR >(MN),
69 _use_binary_join_tree_(use_binary_join_tree) {
70 // create a default triangulation (the user can change it afterwards)
71 _triangulation_ = new DefaultTriangulation;
72
73 // for each node in the MRF, assign the set of factors that contain it
74 const auto& graph = this->MRF().internalGraph();
75 _node_to_factors_.resize(graph.size());
76 _TensorSet_ empty;
77 for (const auto node: graph)
78 _node_to_factors_.insert(node, empty);
79 for (const auto& factor: this->MRF().factors()) {
80 for (const auto node: factor.first) {
81 _node_to_factors_[node].insert(factor.second);
82 }
83 }
84
85 // for debugging purposes
86 GUM_CONSTRUCTOR(ShaferShenoyMRFInference);
87 }
88
89 // destructor
90 template < GUM_Numeric GUM_SCALAR >
91 ShaferShenoyMRFInference< GUM_SCALAR >::~ShaferShenoyMRFInference() {
92 // remove all the tensors created during the last message passing
93 for (const auto& pot: _arc_to_created_tensors_)
94 delete pot.second;
95
96 // remove all the tensors in _clique_ss_tensor_ that do not belong
97 // to _clique_tensors_: in this case, those tensors have been
98 // created by combination of the corresponding list of tensors in
99 // _clique_tensors_. In other words, the size of this list is strictly
100 // greater than 1.
101 for (auto pot: _clique_ss_tensor_) {
102 if (_clique_tensors_[pot.first].size() > 1) delete pot.second;
103 }
104
105 for (auto potset: _clique_tensors_) {
106 for (auto pot: potset.second)
107 delete pot;
108 }
109
110 // remove all the posteriors computed
111 for (const auto& pot: _target_posteriors_)
112 delete pot.second;
113 for (const auto& pot: _joint_target_posteriors_)
114 delete pot.second;
115
116 // remove the junction tree and the triangulation algorithm
117 if (_JT_ != nullptr) delete _JT_;
118 if (_junctionTree_ != nullptr) delete _junctionTree_;
119 delete _triangulation_;
120
121 // for debugging purposes
122 GUM_DESTRUCTOR(ShaferShenoyMRFInference);
123 }
124
126 template < GUM_Numeric GUM_SCALAR >
127 void ShaferShenoyMRFInference< GUM_SCALAR >::setTriangulation(
128 const Triangulation& new_triangulation) {
129 delete _triangulation_;
130 _triangulation_ = new_triangulation.newFactory();
131 _is_new_jt_needed_ = true;
132 this->setOutdatedStructureState_();
133 }
134
136 template < GUM_Numeric GUM_SCALAR >
137 const JoinTree* ShaferShenoyMRFInference< GUM_SCALAR >::joinTree() {
138 if (_is_new_jt_needed_) _createNewJT_();
139
140 return _JT_;
141 }
142
144 template < GUM_Numeric GUM_SCALAR >
145 const JunctionTree* ShaferShenoyMRFInference< GUM_SCALAR >::junctionTree() {
146 if (_is_new_jt_needed_) _createNewJT_();
147
148 return _junctionTree_;
149 }
150
152 template < GUM_Numeric GUM_SCALAR >
153 void ShaferShenoyMRFInference< GUM_SCALAR >::_setProjectionFunction_(
154 Tensor< GUM_SCALAR > (*proj)(const Tensor< GUM_SCALAR >&, const gum::VariableSet&)) {
155 _projection_op_ = proj;
156
157 // indicate that all messages need be reconstructed to take into account
158 // the change in of the projection operator
159 _invalidateAllMessages_();
160 }
161
163 template < GUM_Numeric GUM_SCALAR >
164 void ShaferShenoyMRFInference< GUM_SCALAR >::_setCombinationFunction_(
165 Tensor< GUM_SCALAR > (*comb)(const Tensor< GUM_SCALAR >&, const Tensor< GUM_SCALAR >&)) {
166 _combination_op_ = comb;
167
168 // indicate that all messages need be reconstructed to take into account
169 // the change of the combination operator
170 _invalidateAllMessages_();
171 }
172
174 template < GUM_Numeric GUM_SCALAR >
175 void ShaferShenoyMRFInference< GUM_SCALAR >::_invalidateAllMessages_() {
176 // remove all the messages computed
177 for (auto& pot: _separator_tensors_)
178 pot.second = nullptr;
179
180 for (auto& mess_computed: _messages_computed_)
181 mess_computed.second = false;
182
183 // remove all the created tensors kept on the arcs
184 for (const auto& pot: _arc_to_created_tensors_)
185 if (pot.second != nullptr) delete pot.second;
186 _arc_to_created_tensors_.clear();
187
188 // remove all the posteriors
189 for (const auto& pot: _target_posteriors_)
190 delete pot.second;
191 _target_posteriors_.clear();
192 for (const auto& pot: _joint_target_posteriors_)
193 delete pot.second;
194 _joint_target_posteriors_.clear();
195
196 // indicate that new messages need be computed
197 if (this->isInferenceReady() || this->isInferenceDone()) this->setOutdatedTensorsState_();
198 }
199
201 template < GUM_Numeric GUM_SCALAR >
202 void ShaferShenoyMRFInference< GUM_SCALAR >::onEvidenceAdded_(const NodeId id,
203 bool isHardEvidence) {
204 // if we have a new hard evidence, this modifies the undigraph over which
205 // the join tree is created. This is also the case if id is not a node of
206 // of the undigraph
207 if (isHardEvidence || !_graph_.exists(id)) _is_new_jt_needed_ = true;
208 else {
209 try {
210 _evidence_changes_.insert(id, EvidenceChangeType::EVIDENCE_ADDED);
211 } catch (DuplicateElement const&) {
212 // here, the evidence change already existed. This necessarily means
213 // that the current saved change is an EVIDENCE_ERASED. So if we
214 // erased the evidence and added some again, this corresponds to an
215 // EVIDENCE_MODIFIED
216 _evidence_changes_[id] = EvidenceChangeType::EVIDENCE_MODIFIED;
217 }
218 }
219 }
220
222 template < GUM_Numeric GUM_SCALAR >
223 void ShaferShenoyMRFInference< GUM_SCALAR >::onEvidenceErased_(const NodeId id,
224 bool isHardEvidence) {
225 // if we delete a hard evidence, this modifies the undigraph over which
226 // the join tree is created.
227 if (isHardEvidence) _is_new_jt_needed_ = true;
228 else {
229 try {
230 _evidence_changes_.insert(id, EvidenceChangeType::EVIDENCE_ERASED);
231 } catch (DuplicateElement const&) {
232 // here, the evidence change already existed and it is necessarily an
233 // EVIDENCE_ADDED or an EVIDENCE_MODIFIED. So, if the evidence has
234 // been added and is now erased, this is similar to not having created
235 // it. If the evidence was only modified, it already existed in the
236 // last inference and we should now indicate that it has been removed.
237 if (_evidence_changes_[id] == EvidenceChangeType::EVIDENCE_ADDED)
238 _evidence_changes_.erase(id);
239 else _evidence_changes_[id] = EvidenceChangeType::EVIDENCE_ERASED;
240 }
241 }
242 }
243
245 template < GUM_Numeric GUM_SCALAR >
246 void ShaferShenoyMRFInference< GUM_SCALAR >::onAllEvidenceErased_(bool has_hard_evidence) {
247 if (has_hard_evidence || !this->hardEvidenceNodes().empty()) _is_new_jt_needed_ = true;
248 else {
249 for (const auto node: this->softEvidenceNodes()) {
250 try {
251 _evidence_changes_.insert(node, EvidenceChangeType::EVIDENCE_ERASED);
252 } catch (DuplicateElement const&) {
253 // here, the evidence change already existed and it is necessarily an
254 // EVIDENCE_ADDED or an EVIDENCE_MODIFIED. So, if the evidence has
255 // been added and is now erased, this is similar to not having created
256 // it. If the evidence was only modified, it already existed in the
257 // last inference and we should now indicate that it has been removed.
258 if (_evidence_changes_[node] == EvidenceChangeType::EVIDENCE_ADDED)
259 _evidence_changes_.erase(node);
260 else _evidence_changes_[node] = EvidenceChangeType::EVIDENCE_ERASED;
261 }
262 }
263 }
264 }
265
267 template < GUM_Numeric GUM_SCALAR >
268 void ShaferShenoyMRFInference< GUM_SCALAR >::onEvidenceChanged_(const NodeId id,
269 bool hasChangedSoftHard) {
270 if (hasChangedSoftHard) _is_new_jt_needed_ = true;
271 else {
272 try {
273 _evidence_changes_.insert(id, EvidenceChangeType::EVIDENCE_MODIFIED);
274 } catch (DuplicateElement const&) {
275 // here, the evidence change already existed and it is necessarily an
276 // EVIDENCE_ADDED. So we should keep this state to indicate that this
277 // evidence is new w.r.t. the last inference
278 }
279 }
280 }
281
283 template < GUM_Numeric GUM_SCALAR >
284 void ShaferShenoyMRFInference< GUM_SCALAR >::onMarginalTargetAdded_(const NodeId id) {}
285
287 template < GUM_Numeric GUM_SCALAR >
288 void ShaferShenoyMRFInference< GUM_SCALAR >::onMarginalTargetErased_(const NodeId id) {}
289
291 template < GUM_Numeric GUM_SCALAR >
292 void ShaferShenoyMRFInference< GUM_SCALAR >::onJointTargetAdded_(const NodeSet& set) {}
293
295 template < GUM_Numeric GUM_SCALAR >
296 void ShaferShenoyMRFInference< GUM_SCALAR >::onJointTargetErased_(const NodeSet& set) {}
297
299 template < GUM_Numeric GUM_SCALAR >
300 void ShaferShenoyMRFInference< GUM_SCALAR >::onAllMarginalTargetsAdded_() {}
301
303 template < GUM_Numeric GUM_SCALAR >
304 void ShaferShenoyMRFInference< GUM_SCALAR >::onAllMarginalTargetsErased_() {}
305
307 template < GUM_Numeric GUM_SCALAR >
308 void ShaferShenoyMRFInference< GUM_SCALAR >::onMRFChanged_(
309 const IMarkovRandomField< GUM_SCALAR >* mn) {}
310
312 template < GUM_Numeric GUM_SCALAR >
313 void ShaferShenoyMRFInference< GUM_SCALAR >::onAllJointTargetsErased_() {}
314
316 template < GUM_Numeric GUM_SCALAR >
317 void ShaferShenoyMRFInference< GUM_SCALAR >::onAllTargetsErased_() {}
318
319 // check whether a new junction tree is really needed for the next inference
320 template < GUM_Numeric GUM_SCALAR >
321 bool ShaferShenoyMRFInference< GUM_SCALAR >::_isNewJTNeeded_() const {
322 // if we do not have a JT or if _new_jt_needed_ is set to true, then
323 // we know that we need to create a new join tree
324 if ((_JT_ == nullptr) || _is_new_jt_needed_) return true;
325
326 // if some targets do not belong to the join tree and, consequently, to the
327 // undirected graph that was used to construct the join tree, then we need
328 // to create a new JT: this may happen due to the nodes that received hard
329 // evidence (which do not belong to the graph).
330 const auto& hard_ev_nodes = this->hardEvidenceNodes();
331 for (const auto node: this->targets()) {
332 if (!_graph_.exists(node) && !hard_ev_nodes.exists(node)) return true;
333 }
334
335 // now, do the same for the joint targets
336 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
337 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
338 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
339 elim_order.insert(JT_elim_order[i], (int)i);
340 NodeSet unobserved_set;
341
342 for (const auto& joint_target: this->jointTargets()) {
343 // here, we need to check that at least one clique contains all the
344 // nodes of the joint target.
345 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
346 int elim_number = std::numeric_limits< int >::max();
347 unobserved_set.clear();
348 for (const auto node: joint_target) {
349 if (!_graph_.exists(node)) {
350 if (!hard_ev_nodes.exists(node)) return true;
351 } else {
352 unobserved_set.insert(node);
353 if (elim_order[node] < elim_number) {
354 elim_number = elim_order[node];
355 first_eliminated_node = node;
356 }
357 }
358 }
359 if (!unobserved_set.empty()) {
360 // here, first_eliminated_node contains the first var (node or one of its
361 // parents) eliminated => the clique created during its elimination
362 // should contain all the nodes in unobserved_set
363 const auto clique_id = _node_to_clique_[first_eliminated_node];
364 const auto& clique = _JT_->clique(clique_id);
365 for (const auto node: unobserved_set) {
366 if (!clique.contains(node)) return true;
367 }
368 }
369 }
370
371 // if some new evidence have been added on nodes that do not belong
372 // to _graph_, then we tensorly have to reconstruct the join tree
373 for (const auto& change: _evidence_changes_) {
374 if ((change.second == EvidenceChangeType::EVIDENCE_ADDED) && !_graph_.exists(change.first))
375 return true;
376 }
377
378 // here, the current JT is exactly what we need for the next inference
379 return false;
380 }
381
383 template < GUM_Numeric GUM_SCALAR >
384 void ShaferShenoyMRFInference< GUM_SCALAR >::_createNewJT_() {
385 // to create the JT, we first create the required subgraph of the MRF in the
386 // following way, in order to take into account the nodes that received
387 // evidence:
388 // 1/ we copy the graph of the MRF
389 // 2/ add edges so that joint targets form a clique of the graph
390 // 3/ remove the nodes that received hard evidence
391 //
392 // At the end of step 3/, we have our required graph and we can triangulate
393 // it to get the new junction tree
394
395 // 1/ copy the undirected graph of the MRF
396 const auto& mn = this->MRF();
397 _graph_ = mn.internalGraph();
398
399 // 2/ if there exist some joint targets, we shall add new edges into the
400 // undirected graph in order to ensure that there exists a clique containing
401 // each joint target
402 for (const auto& nodeset: this->jointTargets()) {
403 for (auto iter1 = nodeset.cbegin(); iter1 != nodeset.cend(); ++iter1) {
404 auto iter2 = iter1;
405 for (++iter2; iter2 != nodeset.cend(); ++iter2) {
406 _graph_.addEdge(*iter1, *iter2);
407 }
408 }
409 }
410
411 // 3/ remove all the nodes that received hard evidence
412 _hard_ev_nodes_ = this->hardEvidenceNodes();
413 for (const auto node: _hard_ev_nodes_) {
414 _graph_.eraseNode(node);
415 }
416
417
418 // now, we can compute the new junction tree. To speed-up computations
419 // (essentially, those of a distribution phase), we construct from this
420 // junction tree a binary join tree
421 if (_JT_ != nullptr) delete _JT_;
422 if (_junctionTree_ != nullptr) delete _junctionTree_;
423
424 const auto& domain_sizes = this->domainSizes();
425 _triangulation_->setGraph(&_graph_, &domain_sizes);
426 const JunctionTree& triang_jt = _triangulation_->junctionTree();
427 if (_use_binary_join_tree_) {
428 BinaryJoinTreeConverterDefault bjt_converter;
429 NodeSet emptyset;
430 _JT_ = new CliqueGraph(bjt_converter.convert(triang_jt, domain_sizes, emptyset));
431 } else {
432 _JT_ = new CliqueGraph(triang_jt);
433 }
434 _junctionTree_ = new CliqueGraph(triang_jt);
435
436
437 // assign to each node the order in which it was eliminated by the triangulation
438 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
439 Size size_elim_order = JT_elim_order.size();
440 NodeProperty< int > elim_order(size_elim_order);
441 for (Idx i = Idx(0); i < size_elim_order; ++i)
442 elim_order.insert(JT_elim_order[i], (int)i);
443
444 // assign to each factor of the Markov random field a clique in _JT_
445 // that can contain its conditional probability table
446 _factor_to_clique_.clear();
447 _factor_to_clique_.resize(mn.factors().size());
448 for (const auto& factor: mn.factors()) {
449 const auto& nodes = factor.first; // factor.second is the Tensor()
450 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
451 int elim_number = std::numeric_limits< int >::max();
452 for (const auto node: nodes) {
453 if (_graph_.exists(node) && (elim_order[node] < elim_number)) {
454 elim_number = elim_order[node];
455 first_eliminated_node = node;
456 }
457 }
458
459 if (elim_number != std::numeric_limits< int >::max()) {
460 // first_eliminated_node contains the first var/node eliminated => the
461 // clique created during its elimination must contain node and all of its
462 // neighbors => it necessarily contains all the nodes of factor
463 _factor_to_clique_.insert(
464 factor.second,
465 _triangulation_->createdJunctionTreeClique(first_eliminated_node));
466 }
467 }
468
469 // assign to each node that did not receive some hard evidence the smallest
470 // clique that contains it
471 _node_to_clique_.clear();
472 _node_to_clique_.resize(_graph_.size());
473 NodeProperty< double > node_to_clique_size(_graph_.size());
474 for (const auto node: _graph_) {
475 _node_to_clique_.insert(node, std::numeric_limits< NodeId >::max());
476 node_to_clique_size.insert(node, std::numeric_limits< double >::max());
477 }
478 double overall_size = 0;
479 for (const auto clique_id: *_JT_) {
480 // determine the size of clique_id
481 const auto& clique_nodes = _JT_->clique(clique_id);
482 double clique_size = 1.0;
483 for (const auto node: clique_nodes)
484 clique_size *= double(domain_sizes[node]);
485 overall_size += clique_size;
486
487 // assign the clique to the nodes if its size is smaller than that of the
488 // current cliques assigned to them
489 for (const auto node: clique_nodes) {
490 if (clique_size < node_to_clique_size[node]) {
491 _node_to_clique_[node] = clique_id;
492 node_to_clique_size[node] = clique_size;
493 }
494 }
495 }
496
497 // indicate for each joint_target a clique that contains it
498 _joint_target_to_clique_.clear();
499 for (const auto& set: this->jointTargets()) {
500 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
501 int elim_number = std::numeric_limits< int >::max();
502
503 // do not take into account the nodes that received hard evidence
504 // (since they do not belong to the join tree)
505 for (const auto node: set) {
506 if (!_hard_ev_nodes_.contains(node)) {
507 // the clique we are looking for is the one that was created when
508 // the first element of nodeset was eliminated
509 if (elim_order[node] < elim_number) {
510 elim_number = elim_order[node];
511 first_eliminated_node = node;
512 }
513 }
514 }
515
516 if (elim_number != std::numeric_limits< int >::max()) {
517 _joint_target_to_clique_.insert(
518 set,
519 _triangulation_->createdJunctionTreeClique(first_eliminated_node));
520 }
521 }
522
523 // compute the roots of _JT_'s connected components
524 _computeJoinTreeRoots_();
525
526 // remove all the tensors stored into the cliques. Note that these include
527 // the factors resulting from the projections of hard evidence as well as the
528 // CPTs of the soft evidence
529 for (const auto& pot: _clique_ss_tensor_) {
530 if (_clique_tensors_[pot.first].size() > 1) delete pot.second;
531 }
532 _clique_ss_tensor_.clear();
533 for (const auto& potlist: _clique_tensors_)
534 for (const auto pot: potlist.second)
535 delete pot;
536 _clique_tensors_.clear();
537
538 // remove all the tensors created during the last inference
539 for (const auto& pot: _arc_to_created_tensors_)
540 delete pot.second;
541 _arc_to_created_tensors_.clear();
542
543 // remove all the tensors created to take into account hard evidence
544 // during the last inference (they have already been deleted from memory
545 // by the clearing of _clique_tensors_).
546 _hard_ev_projected_factors_.clear();
547
548 // remove all the soft evidence.
549 _node_to_soft_evidence_.clear();
550
551 // create empty tensor lists into the cliques of the joint tree as well
552 // as empty lists of evidence
553 _ScheduleMultiDimSet_ empty_set;
554 for (const auto node: *_JT_) {
555 _clique_tensors_.insert(node, empty_set);
556 _clique_ss_tensor_.insert(node, nullptr);
557 }
558
559 // remove all the constants created due to projections of CPTs that were
560 // defined over only hard evidence nodes
561 _constants_.clear();
562
563 // create empty messages and indicate that no message has been computed yet
564 _separator_tensors_.clear();
565 _messages_computed_.clear();
566 for (const auto& edge: _JT_->edges()) {
567 const Arc arc1(edge.first(), edge.second());
568 _separator_tensors_.insert(arc1, nullptr);
569 _messages_computed_.insert(arc1, false);
570 const Arc arc2(edge.second(), edge.first());
571 _separator_tensors_.insert(arc2, nullptr);
572 _messages_computed_.insert(arc2, false);
573 }
574
575 // remove all the posteriors computed so far
576 for (const auto& pot: _target_posteriors_)
577 delete pot.second;
578 _target_posteriors_.clear();
579 for (const auto& pot: _joint_target_posteriors_)
580 delete pot.second;
581 _joint_target_posteriors_.clear();
582
583 // here, we determine whether we should use schedules during the inference.
584 // the rule is: if the sum of the domain sizes of the cliques is greater
585 // than a threshold, use schedules
586 _use_schedules_ = (overall_size > _schedule_threshold_);
587
588 // we shall now add all the tensors of the soft evidence to the cliques
589 const NodeProperty< const Tensor< GUM_SCALAR >* >& evidence = this->evidence();
590 for (const auto node: this->softEvidenceNodes()) {
591 if (auto p = _node_to_clique_.tryGet(node)) {
592 auto ev_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(*evidence[node], false);
593 _node_to_soft_evidence_.insert(node, ev_pot);
594 _clique_tensors_[*p].insert(ev_pot);
595 }
596 }
597
598 // put all the factors of the MRF into the cliques
599 // here, beware: all the tensors that are defined over some nodes
600 // including hard evidence must be projected so that these nodes are
601 // removed from the tensor
602 if (_use_schedules_) {
603 Schedule schedule;
604 _initializeJTCliques_(schedule);
605 } else {
606 _initializeJTCliques_();
607 }
608
609
610 // indicate that the data structures are up to date.
611 _evidence_changes_.clear();
612 _is_new_jt_needed_ = false;
613 }
614
616 template < GUM_Numeric GUM_SCALAR >
617 void ShaferShenoyMRFInference< GUM_SCALAR >::_initializeJTCliques_() {
618 const auto& mn = this->MRF();
619
620 // put all the factors of the MRF into the cliques
621 // here, beware: all the tensors that are defined over some nodes
622 // including hard evidence must be projected so that these nodes are
623 // removed from the tensor
624 const NodeProperty< const Tensor< GUM_SCALAR >* >& evidence = this->evidence();
625 const NodeProperty< Idx >& hard_evidence = this->hardEvidence();
626
627 for (const auto& factor: mn.factors()) {
628 const auto& factor_nodes = factor.first;
629 const auto& pot = *(factor.second);
630 const auto& variables = pot.variablesSequence();
631
632 // get the list of nodes with hard evidence in the factor
633 NodeSet hard_nodes(factor_nodes.size());
634 bool graph_contains_nodes = false;
635 for (const auto node: factor_nodes) {
636 if (_hard_ev_nodes_.contains(node)) hard_nodes.insert(node);
637 else if (_graph_.exists(node)) graph_contains_nodes = true;
638 }
639
640 // if hard_nodes contains hard evidence nodes, perform a projection
641 // and insert the result into the appropriate clique, else insert
642 // directly pot into the clique
643 if (hard_nodes.empty()) {
644 auto sched_cpt = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(pot, false);
645 _clique_tensors_[_factor_to_clique_[&pot]].insert(sched_cpt);
646 } else {
647 // marginalize out the hard evidence nodes: if factor_nodes is defined
648 // only over nodes that received hard evidence, do not consider it
649 // as a tensor anymore but as a constant
650 // TODO substitute constants by 0-dimensional tensors
651 if (hard_nodes.size() == factor_nodes.size()) {
652 Instantiation inst(pot);
653 for (Size i = 0; i < hard_nodes.size(); ++i) {
654 inst.chgVal(*variables[i], hard_evidence[mn.nodeId(*(variables[i]))]);
655 }
656 _constants_.insert(&pot, pot.get(inst));
657 } else {
658 // here, we have a factor defined over some nodes that received hard
659 // evidence and other nodes that did not receive it. If none of the
660 // latter belong to the graph, then the factor is useless for inference
661 if (!graph_contains_nodes) continue;
662
663 // prepare the projection with a combine and project instance
664 gum::VariableSet hard_variables;
665 _TensorSet_ marg_factor_set(1 + hard_nodes.size());
666 marg_factor_set.insert(&pot);
667 for (const auto node: hard_nodes) {
668 marg_factor_set.insert(evidence[node]);
669 hard_variables.insert(&(mn.variable(node)));
670 }
671
672 // perform the combination of those tensors and their projection
673 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
674 _combination_op_,
675 _projection_op_);
676
677 _TensorSet_ new_factor_list
678 = combine_and_project.execute(marg_factor_set, hard_variables);
679
680 // there should be only one tensor in new_factor_list
681 if (new_factor_list.size() != 1) {
682 for (const auto pot: new_factor_list) {
683 if (!marg_factor_set.contains(pot)) delete pot;
684 }
686 "the projection of a tensor containing " << "hard evidence is empty!");
687 }
688 auto new_factor = const_cast< Tensor< GUM_SCALAR >* >(*(new_factor_list.begin()));
689 auto projected_factor
690 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*new_factor));
691 delete new_factor;
692
693 _clique_tensors_[_factor_to_clique_[&pot]].insert(projected_factor);
694 _hard_ev_projected_factors_.insert(&pot, projected_factor);
695 }
696 }
697 }
698
699 // now, in _clique_tensors_, for each clique, we have the list of
700 // tensors that must be combined in order to produce the Shafer-Shenoy's
701 // tensor stored into the clique. So, perform this combination and
702 // store the result in _clique_ss_tensor_
703 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
704 for (const auto& xpotset: _clique_tensors_) {
705 const auto& potset = xpotset.second;
706 if (potset.size() > 0) {
707 // here, there will be an entry in _clique_ss_tensor_
708 // If there is only one element in potset, this element shall be
709 // stored into _clique_ss_tensor_, else all the elements of potset
710 // shall be combined and their result shall be stored
711 if (potset.size() == 1) {
712 _clique_ss_tensor_[xpotset.first] = *(potset.cbegin());
713 } else {
714 _TensorSet_ p_potset(potset.size());
715 for (const auto pot: potset)
716 p_potset.insert(
717 &(static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot)->multiDim()));
718
719 Tensor< GUM_SCALAR >* joint
720 = const_cast< Tensor< GUM_SCALAR >* >(fast_combination.execute(p_potset));
721 _clique_ss_tensor_[xpotset.first]
722 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*joint));
723 delete joint;
724 }
725 }
726 }
727 }
728
730 template < GUM_Numeric GUM_SCALAR >
731 void ShaferShenoyMRFInference< GUM_SCALAR >::_initializeJTCliques_(Schedule& schedule) {
732 const auto& mn = this->MRF();
733
734 // put all the factors of the MRF into the cliques
735 // here, beware: all the tensors that are defined over some nodes
736 // including hard evidence must be projected so that these nodes are
737 // removed from the tensor
738 const NodeProperty< const Tensor< GUM_SCALAR >* >& evidence = this->evidence();
739 const NodeProperty< Idx >& hard_evidence = this->hardEvidence();
740
741 for (const auto& factor: mn.factors()) {
742 const auto& factor_nodes = factor.first;
743 const auto& pot = *(factor.second);
744 const auto& variables = pot.variablesSequence();
745
746 // get the list of nodes with hard evidence in the factor
747 NodeSet hard_nodes;
748 bool graph_contains_nodes = false;
749 for (const auto node: factor_nodes) {
750 if (_hard_ev_nodes_.contains(node)) hard_nodes.insert(node);
751 else if (_graph_.exists(node)) graph_contains_nodes = true;
752 }
753
754 // if hard_nodes contains hard evidence nodes, perform a projection
755 // and insert the result into the appropriate clique, else insert
756 // directly pot into the clique
757 if (hard_nodes.empty()) {
758 auto sched_cpt = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(pot, false);
759 _clique_tensors_[_factor_to_clique_[&pot]].insert(sched_cpt);
760 } else {
761 // marginalize out the hard evidence nodes: if factor_nodes is defined
762 // only over nodes that received hard evidence, do not consider it
763 // as a tensor anymore but as a constant
764 // TODO substitute constants by 0-dimensional tensors
765 if (hard_nodes.size() == factor_nodes.size()) {
766 Instantiation inst(pot);
767 for (Size i = 0; i < hard_nodes.size(); ++i) {
768 inst.chgVal(*variables[i], hard_evidence[mn.nodeId(*(variables[i]))]);
769 }
770 _constants_.insert(&pot, pot.get(inst));
771 } else {
772 // here, we have a factor defined over some nodes that received hard
773 // evidence and other nodes that did not receive it. If none of the
774 // latter belong to the graph, then the factor is useless for inference
775 if (!graph_contains_nodes) continue;
776
777 // prepare the projection with a combine and project instance
778 gum::VariableSet hard_variables;
779 _ScheduleMultiDimSet_ marg_factor_set(1 + hard_nodes.size());
780 const IScheduleMultiDim* sched_pot
781 = schedule.insertTable< Tensor< GUM_SCALAR > >(pot, false);
782 marg_factor_set.insert(sched_pot);
783
784 for (const auto node: hard_nodes) {
785 const IScheduleMultiDim* pot
786 = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[node], false);
787 marg_factor_set.insert(pot);
788 hard_variables.insert(&(mn.variable(node)));
789 }
790
791 // perform the combination of those tensors and their projection
792 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
793 _combination_op_,
794 _projection_op_);
795
796 _ScheduleMultiDimSet_ new_factor_list
797 = combine_and_project.schedule(schedule, marg_factor_set, hard_variables);
798
799 // there should be only one tensor in new_factor_list
800 if (new_factor_list.size() != 1) {
802 "the projection of a tensor containing " << "hard evidence is empty!");
803 }
804 auto projected_factor = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
805 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
806 *new_factor_list.begin()));
807 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(projected_factor))
808 ->makeResultsPersistent(true);
809
810 _clique_tensors_[_factor_to_clique_[&pot]].insert(projected_factor);
811 _hard_ev_projected_factors_.insert(&pot, projected_factor);
812 }
813 }
814 }
815 this->scheduler().execute(schedule);
816
817 // now, in _clique_tensors_, for each clique, we have the list of
818 // tensors that must be combined in order to produce the Shafer-Shenoy's
819 // tensor stored into the clique. So, perform this combination and
820 // store the result in _clique_ss_tensor_
821 schedule.clear();
822 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
823 for (const auto& xpotset: _clique_tensors_) {
824 const auto& potset = xpotset.second;
825 if (potset.size() > 0) {
826 // here, there will be an entry in _clique_ss_tensor_
827 // If there is only one element in potset, this element shall be
828 // stored into _clique_ss_tensor_, else all the elements of potset
829 // shall be combined and their result shall be stored
830 if (potset.size() == 1) {
831 _clique_ss_tensor_[xpotset.first] = *(potset.cbegin());
832 } else {
833 // add the tables to combine into the schedule
834 for (const auto pot: potset) {
835 schedule.emplaceScheduleMultiDim(*pot);
836 }
837
838 auto joint = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
839 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
840 fast_combination.schedule(schedule, potset)));
841 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(joint))
842 ->makeResultsPersistent(true);
843 _clique_ss_tensor_[xpotset.first] = joint;
844 }
845 }
846 }
847 this->scheduler().execute(schedule);
848 }
849
851 template < GUM_Numeric GUM_SCALAR >
852 void ShaferShenoyMRFInference< GUM_SCALAR >::updateOutdatedStructure_() {
853 // check if a new JT is really needed. If so, create it
854 if (_isNewJTNeeded_()) {
855 _createNewJT_();
856 } else {
857 // here, we can answer the next queries without reconstructing all the
858 // junction tree. All we need to do is to indicate that we should
859 // update the tensors and messages for these queries
860 updateOutdatedTensors_();
861 }
862 }
863
865 template < GUM_Numeric GUM_SCALAR >
866 void ShaferShenoyMRFInference< GUM_SCALAR >::_diffuseMessageInvalidations_(
867 NodeId from_id,
868 NodeId to_id,
869 NodeSet& invalidated_cliques) {
870 // invalidate the current clique
871 invalidated_cliques.insert(to_id);
872
873 // invalidate the current arc
874 const Arc arc(from_id, to_id);
875 bool& message_computed = _messages_computed_[arc];
876 if (message_computed) {
877 message_computed = false;
878 _separator_tensors_[arc] = nullptr;
879 if (_arc_to_created_tensors_.exists(arc)) {
880 delete _arc_to_created_tensors_[arc];
881 _arc_to_created_tensors_.erase(arc);
882 }
883
884 // go on with the diffusion
885 for (const auto node_id: _JT_->neighbours(to_id)) {
886 if (node_id != from_id) _diffuseMessageInvalidations_(to_id, node_id, invalidated_cliques);
887 }
888 }
889 }
890
893 template < GUM_Numeric GUM_SCALAR >
894 void ShaferShenoyMRFInference< GUM_SCALAR >::updateOutdatedTensors_() {
895 // for each clique, indicate whether the tensor stored into
896 // _clique_ss_tensor_[clique] is the result of a combination. In this
897 // case, it has been allocated by the combination and will need to be
898 // deallocated if its clique has been invalidated
899 NodeProperty< bool > ss_tensor_to_deallocate(_clique_tensors_.size());
900 for (const auto& potset: _clique_tensors_) {
901 ss_tensor_to_deallocate.insert(potset.first, (potset.second.size() > 1));
902 }
903
904 // compute the set of factors that were projected due to hard evidence and
905 // whose hard evidence have changed, so that they need a new projection.
906 // By the way, remove these factors since they are no more needed
907 // Here only the values of the hard evidence can have changed (else a
908 // fully new join tree would have been computed).
909 // Note also that we know that the factors still contain some variable(s) after
910 // the projection (else they should be constants)
911 const auto& mn = this->MRF();
912 NodeSet hard_nodes_changed(_hard_ev_nodes_.size());
913 Set< const Tensor< GUM_SCALAR >* > hard_projected_factors_changed(mn.factors().size());
914 for (const auto node: _hard_ev_nodes_) {
915 if (_evidence_changes_.exists(node)) {
916 hard_nodes_changed.insert(node);
917 for (const auto pot: _node_to_factors_[node]) {
918 if (_hard_ev_projected_factors_.exists(pot)
919 && !hard_projected_factors_changed.exists(pot)) {
920 hard_projected_factors_changed.insert(pot);
921 }
922 }
923 }
924 }
925
926 NodeSet hard_cliques_changed(hard_projected_factors_changed.size());
927 for (const auto pot: hard_projected_factors_changed) {
928 const auto chgPot = _hard_ev_projected_factors_[pot];
929 const NodeId chgClique = _factor_to_clique_[pot];
930 _clique_tensors_[chgClique].erase(chgPot);
931 _hard_ev_projected_factors_.erase(pot);
932 if (!hard_cliques_changed.contains(chgClique)) hard_cliques_changed.insert(chgClique);
933 delete chgPot;
934 }
935
936
937 // invalidate all the messages that are no more correct: start from each of
938 // the nodes whose soft evidence has changed and perform a diffusion from
939 // the clique into which the soft evidence has been entered, indicating that
940 // the messages spreading from this clique are now invalid. At the same time,
941 // if there were tensors created on the arcs over which the messages were
942 // sent, remove them from memory. For all the cliques that received some
943 // projected factors that should now be changed, do the same.
944 NodeSet invalidated_cliques(_JT_->size());
945 for (const auto& pair: _evidence_changes_) {
946 if (auto p = _node_to_clique_.tryGet(pair.first)) {
947 const auto clique = *p;
948 invalidated_cliques.insert(clique);
949 for (const auto neighbor: _JT_->neighbours(clique)) {
950 _diffuseMessageInvalidations_(clique, neighbor, invalidated_cliques);
951 }
952 }
953 }
954
955 // now, add to the set of invalidated cliques those that contain projected
956 // factors that were changed.
957 for (const auto clique: hard_cliques_changed) {
958 invalidated_cliques.insert(clique);
959 for (const auto neighbor: _JT_->neighbours(clique)) {
960 _diffuseMessageInvalidations_(clique, neighbor, invalidated_cliques);
961 }
962 }
963
964 // now that we know the cliques whose set of tensors have been changed,
965 // we can discard their corresponding Shafer-Shenoy tensor
966 for (const auto clique: invalidated_cliques) {
967 if (ss_tensor_to_deallocate[clique]) {
968 delete _clique_ss_tensor_[clique];
969 _clique_ss_tensor_[clique] = nullptr;
970 }
971 }
972
973
974 // now we shall remove all the posteriors that belong to the
975 // invalidated cliques.
976 if (!_target_posteriors_.empty()) {
977 for (auto iter = _target_posteriors_.beginSafe(); iter != _target_posteriors_.endSafe();
978 ++iter) {
979 // first, cope only with the nodes that did not receive hard evidence
980 // since the other nodes do not belong to the join tree
981 if (_graph_.exists(iter.key())
982 && (invalidated_cliques.exists(_node_to_clique_[iter.key()]))) {
983 delete iter.val();
984 _target_posteriors_.erase(iter);
985 }
986 // now cope with the nodes that received hard evidence
987 else if (hard_nodes_changed.contains(iter.key())) {
988 delete iter.val();
989 _target_posteriors_.erase(iter);
990 }
991 }
992 }
993
994 // finally, cope with joint targets. Notably, remove the joint posteriors whose
995 // nodes have all received changed evidence
996 for (auto iter = _joint_target_posteriors_.beginSafe();
997 iter != _joint_target_posteriors_.endSafe();
998 ++iter) {
999 if (invalidated_cliques.exists(_joint_target_to_clique_[iter.key()])) {
1000 delete iter.val();
1001 _joint_target_posteriors_.erase(iter);
1002 } else {
1003 // check for sets in which all nodes have received evidence
1004 bool has_unevidenced_node = false;
1005 for (const auto node: iter.key()) {
1006 if (!hard_nodes_changed.exists(node)) {
1007 has_unevidenced_node = true;
1008 break;
1009 }
1010 }
1011 if (!has_unevidenced_node) {
1012 delete iter.val();
1013 _joint_target_posteriors_.erase(iter);
1014 }
1015 }
1016 }
1017
1018 // remove all the evidence that were entered into _node_to_soft_evidence_
1019 // and _clique_ss_tensor_ and add the new soft ones
1020 for (const auto& pot_pair: _node_to_soft_evidence_) {
1021 delete pot_pair.second;
1022 _clique_tensors_[_node_to_clique_[pot_pair.first]].erase(pot_pair.second);
1023 }
1024 _node_to_soft_evidence_.clear();
1025
1026 const auto& evidence = this->evidence();
1027 for (const auto node: this->softEvidenceNodes()) {
1028 auto ev_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(*evidence[node], false);
1029 _node_to_soft_evidence_.insert(node, ev_pot);
1030 _clique_tensors_[_node_to_clique_[node]].insert(ev_pot);
1031 }
1032
1033
1034 // Now add the projections of the factors due to newly changed hard evidence:
1035 // if we are performing updateOutdatedTensors_, this means that the
1036 // set of nodes that received hard evidence has not changed, only
1037 // their instantiations can have changed. So, if there is an entry
1038 // for node in _constants_, there will still be such an entry after
1039 // performing the new projections. Idem for _hard_ev_projected_factors_
1040 if (_use_schedules_) {
1041 Schedule schedule;
1042 for (const auto pot: hard_projected_factors_changed) {
1043 _ScheduleMultiDimSet_ marg_pot_set;
1044 const auto sched_pot = schedule.insertTable< Tensor< GUM_SCALAR > >(*pot, false);
1045 marg_pot_set.insert(sched_pot);
1046 const auto& variables = pot->variablesSequence();
1047 gum::VariableSet hard_variables(variables.size());
1048 for (const auto var: variables) {
1049 NodeId xnode = mn.nodeId(*var);
1050 if (_hard_ev_nodes_.exists(xnode)) {
1051 const auto ev_pot
1052 = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[xnode], false);
1053 marg_pot_set.insert(ev_pot);
1054 hard_variables.insert(var);
1055 }
1056 }
1057
1058 // perform the combination of those tensors and their projection
1059 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
1060 _combination_op_,
1061 _projection_op_);
1062
1063 _ScheduleMultiDimSet_ new_pot_list
1064 = combine_and_project.schedule(schedule, marg_pot_set, hard_variables);
1065
1066 // there should be only one tensor in new_cpt_list
1067 if (new_pot_list.size() != 1) {
1069 "the projection of a tensor containing " << "hard evidence is empty!");
1070 }
1071 auto projected_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1072 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(*new_pot_list.begin()));
1073 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(projected_pot))
1074 ->makeResultsPersistent(true);
1075 _clique_tensors_[_factor_to_clique_[pot]].insert(projected_pot);
1076 _hard_ev_projected_factors_.insert(pot, projected_pot);
1077 }
1078
1079 // here, the list of tensors stored in the invalidated cliques have
1080 // been updated. So, now, we can combine them to produce the Shafer-Shenoy
1081 // tensor stored into the clique
1082 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1083 for (const auto clique: invalidated_cliques) {
1084 const auto& potset = _clique_tensors_[clique];
1085
1086 if (potset.size() > 0) {
1087 // here, there will be an entry in _clique_ss_tensor_
1088 // If there is only one element in potset, this element shall be
1089 // stored into _clique_ss_tensor_, else all the elements of potset
1090 // shall be combined and their result shall be stored
1091 if (potset.size() == 1) {
1092 _clique_ss_tensor_[clique] = *(potset.cbegin());
1093 } else {
1094 for (const auto pot: potset)
1095 if (!schedule.existsScheduleMultiDim(pot->id()))
1096 schedule.emplaceScheduleMultiDim(*pot);
1097 auto joint = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1098 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1099 fast_combination.schedule(schedule, potset)));
1100 const_cast< ScheduleOperator* >(schedule.scheduleMultiDimCreator(joint))
1101 ->makeResultsPersistent(true);
1102 _clique_ss_tensor_[clique] = joint;
1103 }
1104 }
1105 }
1106 this->scheduler().execute(schedule);
1107 } else {
1108 for (const auto pot: hard_projected_factors_changed) {
1109 _TensorSet_ marg_pot_set;
1110 marg_pot_set.insert(pot);
1111 const auto& variables = pot->variablesSequence();
1112
1113 gum::VariableSet hard_variables(variables.size());
1114 for (const auto var: variables) {
1115 NodeId xnode = mn.nodeId(*var);
1116 if (_hard_ev_nodes_.exists(xnode)) {
1117 marg_pot_set.insert(evidence[xnode]);
1118 hard_variables.insert(var);
1119 }
1120 }
1121
1122 // perform the combination of those tensors and their projection
1123 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
1124 _combination_op_,
1125 _projection_op_);
1126
1127 _TensorSet_ new_pot_list = combine_and_project.execute(marg_pot_set, hard_variables);
1128
1129 // there should be only one tensor in new_cpt_list
1130 if (new_pot_list.size() != 1) {
1132 "the projection of a tensor containing " << "hard evidence is empty!");
1133 }
1134 Tensor< GUM_SCALAR >* xprojected_pot
1135 = const_cast< Tensor< GUM_SCALAR >* >(*new_pot_list.begin());
1136 auto projected_pot
1137 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*xprojected_pot));
1138 delete xprojected_pot;
1139 _clique_tensors_[_factor_to_clique_[pot]].insert(projected_pot);
1140 _hard_ev_projected_factors_.insert(pot, projected_pot);
1141 }
1142
1143 // here, the list of tensors stored in the invalidated cliques have
1144 // been updated. So, now, we can combine them to produce the Shafer-Shenoy
1145 // tensor stored into the clique
1146 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1147 for (const auto clique: invalidated_cliques) {
1148 const auto& potset = _clique_tensors_[clique];
1149
1150 if (potset.size() > 0) {
1151 // here, there will be an entry in _clique_ss_tensor_
1152 // If there is only one element in potset, this element shall be
1153 // stored into _clique_ss_tensor_, else all the elements of potset
1154 // shall be combined and their result shall be stored
1155 if (potset.size() == 1) {
1156 _clique_ss_tensor_[clique] = *(potset.cbegin());
1157 } else {
1158 _TensorSet_ p_potset(potset.size());
1159 for (const auto pot: potset)
1160 p_potset.insert(&(
1161 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot)->multiDim()));
1162
1163 Tensor< GUM_SCALAR >* joint
1164 = const_cast< Tensor< GUM_SCALAR >* >(fast_combination.execute(p_potset));
1165 _clique_ss_tensor_[clique]
1166 = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(std::move(*joint));
1167 delete joint;
1168 }
1169 }
1170 }
1171 }
1172
1173 // update the constants
1174 const auto& hard_evidence = this->hardEvidence();
1175 for (auto& node_cst: _constants_) {
1176 const Tensor< GUM_SCALAR >& pot = *(node_cst.first);
1177 Instantiation inst(pot);
1178 for (const auto var: pot.variablesSequence()) {
1179 inst.chgVal(*var, hard_evidence[mn.nodeId(*var)]);
1180 }
1181 node_cst.second = pot.get(inst);
1182 }
1183
1184 // indicate that all changes have been performed
1185 _evidence_changes_.clear();
1186 }
1187
1189 template < GUM_Numeric GUM_SCALAR >
1190 void ShaferShenoyMRFInference< GUM_SCALAR >::_computeJoinTreeRoots_() {
1191 // get the set of cliques in which we can find the targets and joint_targets.
1192 // Due to hard evidence, the cliques related to a given target node
1193 // might not exist, hence the if checks
1194 NodeSet clique_targets;
1195 for (const auto node: this->targets()) {
1196 if (auto p = _node_to_clique_.tryGet(node)) { clique_targets.insert(*p); }
1197 }
1198 for (const auto& set: this->jointTargets()) {
1199 if (auto p = _joint_target_to_clique_.tryGet(set)) { clique_targets.insert(*p); }
1200 }
1201
1202 // put in a vector these cliques and their sizes
1203 std::vector< std::pair< NodeId, Size > > possible_roots(clique_targets.size());
1204 const auto& mn = this->MRF();
1205 std::size_t i = 0;
1206 for (const auto clique_id: clique_targets) {
1207 const auto& clique = _JT_->clique(clique_id);
1208 Size dom_size = 1;
1209 for (const auto node: clique) {
1210 dom_size *= mn.variable(node).domainSize();
1211 }
1212 possible_roots[i] = std::pair< NodeId, Size >(clique_id, dom_size);
1213 ++i;
1214 }
1215
1216 // sort the cliques by increasing domain size
1217 std::sort(possible_roots.begin(),
1218 possible_roots.end(),
1219 [](const std::pair< NodeId, Size >& a, const std::pair< NodeId, Size >& b) -> bool {
1220 return a.second < b.second;
1221 });
1222
1223 // pick up the clique with the smallest size in each connected component
1224 NodeProperty< bool > marked = _JT_->nodesPropertyFromVal(false);
1225 std::function< void(NodeId, NodeId) > diffuse_marks
1226 = [&marked, &diffuse_marks, this](NodeId node, NodeId from) {
1227 if (!marked[node]) {
1228 marked[node] = true;
1229 for (const auto neigh: _JT_->neighbours(node))
1230 if ((neigh != from) && !marked[neigh]) diffuse_marks(neigh, node);
1231 }
1232 };
1233 _roots_.clear();
1234 for (const auto& xclique: possible_roots) {
1235 NodeId clique = xclique.first;
1236 if (!marked[clique]) {
1237 _roots_.insert(clique);
1238 diffuse_marks(clique, clique);
1239 }
1240 }
1241 }
1242
1243 // performs the collect phase of Shafer-Shenoy using schedules
1244 template < GUM_Numeric GUM_SCALAR >
1245 void ShaferShenoyMRFInference< GUM_SCALAR >::_collectMessage_(Schedule& schedule,
1246 NodeId id,
1247 NodeId from) {
1248 for (const auto other: _JT_->neighbours(id)) {
1249 if ((other != from) && !_messages_computed_[Arc(other, id)])
1250 _collectMessage_(schedule, other, id);
1251 }
1252
1253 if ((id != from) && !_messages_computed_[Arc(id, from)]) {
1254 _produceMessage_(schedule, id, from);
1255 }
1256 }
1257
1258 // performs the collect phase of Shafer-Shenoy without schedules
1259 template < GUM_Numeric GUM_SCALAR >
1260 void ShaferShenoyMRFInference< GUM_SCALAR >::_collectMessage_(NodeId id, NodeId from) {
1261 for (const auto other: _JT_->neighbours(id)) {
1262 if ((other != from) && !_messages_computed_[Arc(other, id)]) _collectMessage_(other, id);
1263 }
1264
1265 if ((id != from) && !_messages_computed_[Arc(id, from)]) { _produceMessage_(id, from); }
1266 }
1267
1268 // remove variables del_vars from the list of tensors pot_list
1269 template < GUM_Numeric GUM_SCALAR >
1270 const IScheduleMultiDim* ShaferShenoyMRFInference< GUM_SCALAR >::_marginalizeOut_(
1271 Schedule& schedule,
1272 Set< const IScheduleMultiDim* > pot_list,
1273 gum::VariableSet& del_vars,
1274 gum::VariableSet& kept_vars) {
1275 // let's guarantee that all the tensors to be combined and projected
1276 // belong to the schedule
1277 for (const auto pot: pot_list) {
1278 if (!schedule.existsScheduleMultiDim(pot->id())) schedule.emplaceScheduleMultiDim(*pot);
1279 }
1280
1281 // create a combine and project operator that will perform the
1282 // marginalization
1283 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(_combination_op_,
1284 _projection_op_);
1285 _ScheduleMultiDimSet_ new_pot_list = combine_and_project.schedule(schedule, pot_list, del_vars);
1286
1287 // combine all the remaining tensors in order to create only one resulting tensor
1288 if (new_pot_list.size() == 1) return *(new_pot_list.begin());
1289 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1290 return fast_combination.schedule(schedule, new_pot_list);
1291 }
1292
1293 // remove variables del_vars from the list of tensors pot_list
1294 template < GUM_Numeric GUM_SCALAR >
1295 const IScheduleMultiDim* ShaferShenoyMRFInference< GUM_SCALAR >::_marginalizeOut_(
1296 Set< const IScheduleMultiDim* >& pot_list,
1297 gum::VariableSet& del_vars,
1298 gum::VariableSet& kept_vars) {
1299 _TensorSet_ xpot_list(pot_list.size());
1300 for (auto pot: pot_list)
1301 xpot_list.insert(
1302 &(static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot)->multiDim()));
1303
1304 // create a combine and project operator that will perform the
1305 // marginalization
1306 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(_combination_op_,
1307 _projection_op_);
1308 _TensorSet_ xnew_pot_list = combine_and_project.execute(xpot_list, del_vars);
1309
1310 // combine all the remaining tensors in order to create only one resulting tensor
1311 const Tensor< GUM_SCALAR >* xres_pot;
1312 if (xnew_pot_list.size() == 1) {
1313 xres_pot = *(xnew_pot_list.begin());
1314 } else {
1315 // combine all the tensors that resulted from the above combine and
1316 // projet execution
1317 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1318 xres_pot = fast_combination.execute(xnew_pot_list);
1319 for (const auto pot: xnew_pot_list) {
1320 if (!xpot_list.contains(pot) && (pot != xres_pot)) delete pot;
1321 }
1322 }
1323
1324 // transform xres_pot into a ScheduleMultiDim
1325 ScheduleMultiDim< Tensor< GUM_SCALAR > >* res_pot;
1326 if (xpot_list.contains(xres_pot))
1327 res_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(*xres_pot, false);
1328 else {
1329 res_pot = new ScheduleMultiDim< Tensor< GUM_SCALAR > >(
1330 std::move(const_cast< Tensor< GUM_SCALAR >& >(*xres_pot)));
1331 delete xres_pot;
1332 }
1333
1334 return res_pot;
1335 }
1336
1337 // creates the message sent by clique from_id to clique to_id
1338 template < GUM_Numeric GUM_SCALAR >
1339 void ShaferShenoyMRFInference< GUM_SCALAR >::_produceMessage_(Schedule& schedule,
1340 NodeId from_id,
1341 NodeId to_id) {
1342 // get the tensors of the clique.
1343 _ScheduleMultiDimSet_ pot_list;
1344 if (_clique_ss_tensor_[from_id] != nullptr) pot_list.insert(_clique_ss_tensor_[from_id]);
1345
1346 // add the messages sent by adjacent nodes to from_id.
1347 for (const auto other_id: _JT_->neighbours(from_id)) {
1348 if (other_id != to_id) {
1349 const auto separator_pot = _separator_tensors_[Arc(other_id, from_id)];
1350 if (separator_pot != nullptr) pot_list.insert(separator_pot);
1351 }
1352 }
1353
1354 // get the set of variables that need be removed from the tensors
1355 const NodeSet& from_clique = _JT_->clique(from_id);
1356 const NodeSet& separator = _JT_->separator(from_id, to_id);
1357 gum::VariableSet del_vars(from_clique.size());
1358 gum::VariableSet kept_vars(separator.size());
1359 const auto& mn = this->MRF();
1360
1361 for (const auto node: from_clique) {
1362 if (!separator.contains(node)) {
1363 del_vars.insert(&(mn.variable(node)));
1364 } else {
1365 kept_vars.insert(&(mn.variable(node)));
1366 }
1367 }
1368
1369 // pot_list now contains all the tensors to multiply and marginalize
1370 // => combine the messages
1371 const IScheduleMultiDim* new_pot = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
1372
1373 // keep track of the newly created tensor
1374 const Arc arc(from_id, to_id);
1375 if (!pot_list.exists(new_pot)) {
1376 if (!_arc_to_created_tensors_.exists(arc)) {
1377 _arc_to_created_tensors_.insert(arc, new_pot);
1378
1379 // do not forget to make the ScheduleMultiDim persistent
1380 auto op = schedule.scheduleMultiDimCreator(new_pot);
1381 if (op != nullptr) const_cast< ScheduleOperator* >(op)->makeResultsPersistent(true);
1382 }
1383 }
1384
1385 _separator_tensors_[arc] = new_pot;
1386 _messages_computed_[arc] = true;
1387 }
1388
1389 // creates the message sent by clique from_id to clique to_id
1390 template < GUM_Numeric GUM_SCALAR >
1391 void ShaferShenoyMRFInference< GUM_SCALAR >::_produceMessage_(NodeId from_id, NodeId to_id) {
1392 // get the tensors of the clique.
1393 _ScheduleMultiDimSet_ pot_list;
1394 if (_clique_ss_tensor_[from_id] != nullptr) pot_list.insert(_clique_ss_tensor_[from_id]);
1395
1396 // add the messages sent by adjacent nodes to from_id.
1397 for (const auto other_id: _JT_->neighbours(from_id)) {
1398 if (other_id != to_id) {
1399 const auto separator_pot = _separator_tensors_[Arc(other_id, from_id)];
1400 if (separator_pot != nullptr) pot_list.insert(separator_pot);
1401 }
1402 }
1403
1404 // get the set of variables that need be removed from the tensors
1405 const NodeSet& from_clique = _JT_->clique(from_id);
1406 const NodeSet& separator = _JT_->separator(from_id, to_id);
1407 gum::VariableSet del_vars(from_clique.size());
1408 gum::VariableSet kept_vars(separator.size());
1409 const auto& mn = this->MRF();
1410
1411 for (const auto node: from_clique) {
1412 if (!separator.contains(node)) {
1413 del_vars.insert(&(mn.variable(node)));
1414 } else {
1415 kept_vars.insert(&(mn.variable(node)));
1416 }
1417 }
1418
1419 // pot_list now contains all the tensors to multiply and marginalize
1420 // => combine the messages
1421 const IScheduleMultiDim* new_pot = _marginalizeOut_(pot_list, del_vars, kept_vars);
1422
1423 // keep track of the newly created tensor
1424 const Arc arc(from_id, to_id);
1425 if (!pot_list.exists(new_pot)) {
1426 if (!_arc_to_created_tensors_.exists(arc)) { _arc_to_created_tensors_.insert(arc, new_pot); }
1427 }
1428
1429 _separator_tensors_[arc] = new_pot;
1430 _messages_computed_[arc] = true;
1431 }
1432
1433 // fired after a new Markov net has been assigned to the inference engine
1434 template < GUM_Numeric GUM_SCALAR >
1435 void ShaferShenoyMRFInference< GUM_SCALAR >::onModelChanged_(const GraphicalModel* mn) {
1436 JointTargetedMRFInference< GUM_SCALAR >::onModelChanged_(mn);
1437 }
1438
1439 // performs a whole inference
1440 template < GUM_Numeric GUM_SCALAR >
1441 void ShaferShenoyMRFInference< GUM_SCALAR >::makeInference_() {
1442 if (_use_schedules_) {
1443 Schedule schedule;
1444
1445 // collect messages for all single targets
1446 for (const auto node: this->targets()) {
1447 // perform only collects in the join tree for nodes that have
1448 // not received hard evidence (those that received hard evidence were
1449 // not included into the join tree for speed-up reasons)
1450 if (_graph_.exists(node)) {
1451 _collectMessage_(schedule, _node_to_clique_[node], _node_to_clique_[node]);
1452 }
1453 }
1454
1455 // collect messages for all set targets
1456 // by parsing _joint_target_to_clique_, we ensure that the cliques that
1457 // are referenced belong to the join tree (even if some of the nodes in
1458 // their associated joint_target do not belong to _graph_)
1459 for (const auto& set: _joint_target_to_clique_)
1460 _collectMessage_(schedule, set.second, set.second);
1461
1462 // really perform the computations
1463 this->scheduler().execute(schedule);
1464 } else {
1465 // collect messages for all single targets
1466 for (const auto node: this->targets()) {
1467 // perform only collects in the join tree for nodes that have
1468 // not received hard evidence (those that received hard evidence were
1469 // not included into the join tree for speed-up reasons)
1470 if (_graph_.exists(node)) {
1471 _collectMessage_(_node_to_clique_[node], _node_to_clique_[node]);
1472 }
1473 }
1474
1475 // collect messages for all set targets
1476 // by parsing _joint_target_to_clique_, we ensure that the cliques that
1477 // are referenced belong to the join tree (even if some of the nodes in
1478 // their associated joint_target do not belong to _graph_)
1479 for (const auto& set: _joint_target_to_clique_)
1480 _collectMessage_(set.second, set.second);
1481 }
1482 }
1483
1485 template < GUM_Numeric GUM_SCALAR >
1486 Tensor< GUM_SCALAR >*
1487 ShaferShenoyMRFInference< GUM_SCALAR >::unnormalizedJointPosterior_(NodeId id) {
1488 if (_use_schedules_) {
1489 Schedule schedule;
1490 return _unnormalizedJointPosterior_(schedule, id);
1491 } else {
1492 return _unnormalizedJointPosterior_(id);
1493 }
1494 }
1495
1497 template < GUM_Numeric GUM_SCALAR >
1498 Tensor< GUM_SCALAR >*
1499 ShaferShenoyMRFInference< GUM_SCALAR >::_unnormalizedJointPosterior_(Schedule& schedule,
1500 NodeId id) {
1501 const auto& mn = this->MRF();
1502
1503 // hard evidence do not belong to the join tree
1504 // # TODO: check for sets of inconsistent hard evidence
1505 if (this->hardEvidenceNodes().contains(id)) {
1506 return new Tensor< GUM_SCALAR >(*(this->evidence()[id]));
1507 }
1508
1509 auto& scheduler = this->scheduler();
1510
1511 // if we still need to perform some inference task, do it (this should
1512 // already have been done by makeInference_)
1513 const NodeId clique_of_id = _node_to_clique_[id];
1514 _collectMessage_(schedule, clique_of_id, clique_of_id);
1515
1516 // now we just need to create the product of the tensors of the clique
1517 // containing id with the messages received by this clique and
1518 // marginalize out all variables except id
1519 _ScheduleMultiDimSet_ pot_list;
1520 if (_clique_ss_tensor_[clique_of_id] != nullptr)
1521 pot_list.insert(_clique_ss_tensor_[clique_of_id]);
1522
1523 // add the messages sent by adjacent nodes to targetClique
1524 for (const auto other: _JT_->neighbours(clique_of_id))
1525 pot_list.insert(_separator_tensors_[Arc(other, clique_of_id)]);
1526
1527 // get the set of variables that need be removed from the tensors
1528 const NodeSet& nodes = _JT_->clique(clique_of_id);
1529 gum::VariableSet kept_vars{&(mn.variable(id))};
1530 gum::VariableSet del_vars(nodes.size());
1531 for (const auto node: nodes) {
1532 if (node != id) del_vars.insert(&(mn.variable(node)));
1533 }
1534
1535 // pot_list now contains all the tensors to multiply and marginalize
1536 // => combine the messages
1537 auto resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1538 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1539 _marginalizeOut_(schedule, pot_list, del_vars, kept_vars)));
1540 Tensor< GUM_SCALAR >* joint = nullptr;
1541
1542 scheduler.execute(schedule);
1543
1544 // if pot already existed, create a copy, so that we can put it into
1545 // the _target_posteriors_ property
1546 if (pot_list.exists(resulting_pot)) {
1547 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
1548 } else {
1549 joint = resulting_pot->exportMultiDim();
1550 }
1551
1552 // check that the joint posterior is different from a 0 vector: this would
1553 // indicate that some hard evidence are not compatible (their joint
1554 // probability is equal to 0)
1555 bool nonzero_found = false;
1556 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1557 if (joint->get(inst)) {
1558 nonzero_found = true;
1559 break;
1560 }
1561 }
1562 if (!nonzero_found) {
1563 // remove joint from memory to avoid memory leaks
1564 delete joint;
1566 "some evidence entered into the Markov "
1567 "net are incompatible (their joint proba = 0)");
1568 }
1569 return joint;
1570 }
1571
1573 template < GUM_Numeric GUM_SCALAR >
1574 Tensor< GUM_SCALAR >*
1575 ShaferShenoyMRFInference< GUM_SCALAR >::_unnormalizedJointPosterior_(NodeId id) {
1576 const auto& mn = this->MRF();
1577
1578 // hard evidence do not belong to the join tree
1579 // # TODO: check for sets of inconsistent hard evidence
1580 if (this->hardEvidenceNodes().contains(id)) {
1581 return new Tensor< GUM_SCALAR >(*(this->evidence()[id]));
1582 }
1583
1584 // if we still need to perform some inference task, do it (this should
1585 // already have been done by makeInference_)
1586 NodeId clique_of_id = _node_to_clique_[id];
1587 _collectMessage_(clique_of_id, clique_of_id);
1588
1589 // now we just need to create the product of the tensors of the clique
1590 // containing id with the messages received by this clique and
1591 // marginalize out all variables except id
1592 _ScheduleMultiDimSet_ pot_list;
1593 if (_clique_ss_tensor_[clique_of_id] != nullptr)
1594 pot_list.insert(_clique_ss_tensor_[clique_of_id]);
1595
1596 // add the messages sent by adjacent nodes to targetClique
1597 for (const auto other: _JT_->neighbours(clique_of_id))
1598 pot_list.insert(_separator_tensors_[Arc(other, clique_of_id)]);
1599
1600 // get the set of variables that need be removed from the tensors
1601 const NodeSet& nodes = _JT_->clique(clique_of_id);
1602 gum::VariableSet kept_vars{&(mn.variable(id))};
1603 gum::VariableSet del_vars(nodes.size());
1604 for (const auto node: nodes) {
1605 if (node != id) del_vars.insert(&(mn.variable(node)));
1606 }
1607
1608 // pot_list now contains all the tensors to multiply and marginalize
1609 // => combine the messages
1610 auto resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1611 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1612 _marginalizeOut_(pot_list, del_vars, kept_vars)));
1613 Tensor< GUM_SCALAR >* joint = nullptr;
1614
1615 // if pot already existed, create a copy, so that we can put it into
1616 // the _target_posteriors_ property
1617 if (pot_list.exists(resulting_pot)) {
1618 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
1619 } else {
1620 joint = resulting_pot->exportMultiDim();
1621 delete resulting_pot;
1622 }
1623
1624 // check that the joint posterior is different from a 0 vector: this would
1625 // indicate that some hard evidence are not compatible (their joint
1626 // probability is equal to 0)
1627 bool nonzero_found = false;
1628 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1629 if (joint->get(inst)) {
1630 nonzero_found = true;
1631 break;
1632 }
1633 }
1634 if (!nonzero_found) {
1635 // remove joint from memory to avoid memory leaks
1636 delete joint;
1638 "some evidence entered into the Markov "
1639 "net are incompatible (their joint proba = 0)");
1640 }
1641 return joint;
1642 }
1643
1645 template < GUM_Numeric GUM_SCALAR >
1646 const Tensor< GUM_SCALAR >& ShaferShenoyMRFInference< GUM_SCALAR >::posterior_(NodeId id) {
1647 // check if we have already computed the posterior
1648 if (auto p = _target_posteriors_.tryGet(id)) { return *(*p); }
1649
1650 // compute the joint posterior and normalize
1651 auto joint = unnormalizedJointPosterior_(id);
1652 if (joint->sum() != 1) // hard test for ReadOnly CPT (as aggregator)
1653 joint->normalize();
1654 _target_posteriors_.insert(id, joint);
1655
1656 return *joint;
1657 }
1658
1660 template < GUM_Numeric GUM_SCALAR >
1661 Tensor< GUM_SCALAR >*
1662 ShaferShenoyMRFInference< GUM_SCALAR >::unnormalizedJointPosterior_(const NodeSet& set) {
1663 if (_use_schedules_) {
1664 Schedule schedule;
1665 return _unnormalizedJointPosterior_(schedule, set);
1666 } else {
1667 return _unnormalizedJointPosterior_(set);
1668 }
1669 }
1670
1672 template < GUM_Numeric GUM_SCALAR >
1673 Tensor< GUM_SCALAR >*
1674 ShaferShenoyMRFInference< GUM_SCALAR >::_unnormalizedJointPosterior_(Schedule& schedule,
1675 const NodeSet& set) {
1676 // hard evidence do not belong to the join tree, so extract the nodes
1677 // from targets that are not hard evidence
1678 NodeSet targets = set, hard_ev_nodes;
1679 for (const auto node: this->hardEvidenceNodes()) {
1680 if (targets.contains(node)) {
1681 targets.erase(node);
1682 hard_ev_nodes.insert(node);
1683 }
1684 }
1685
1686 auto& scheduler = this->scheduler();
1687
1688 // if all the nodes have received hard evidence, then compute the
1689 // joint posterior directly by multiplying the hard evidence tensors
1690 const auto& evidence = this->evidence();
1691 if (targets.empty()) {
1692 if (set.size() == 1) {
1693 return new Tensor< GUM_SCALAR >(*evidence[*set.begin()]);
1694 } else {
1695 _ScheduleMultiDimSet_ pot_list;
1696 for (const auto node: set) {
1697 auto new_pot_ev = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[node], false);
1698 pot_list.insert(new_pot_ev);
1699 }
1700
1701 // combine all the tensors of the nodes in set
1702 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1703 const IScheduleMultiDim* pot = fast_combination.schedule(schedule, pot_list);
1704 auto schedule_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1705 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot));
1706 scheduler.execute(schedule);
1707 auto result = schedule_pot->exportMultiDim();
1708
1709 return result;
1710 }
1711 }
1712
1713
1714 // if we still need to perform some inference task, do it: so, first,
1715 // determine the clique on which we should perform collect to compute
1716 // the unnormalized joint posterior of a set of nodes containing "targets"
1717 NodeId clique_of_set;
1718 if (auto p_clique = _joint_target_to_clique_.tryGet(set)) {
1719 clique_of_set = *p_clique;
1720 } else {
1721 // here, the precise set of targets does not belong to the set of targets
1722 // defined by the user. So we will try to find a clique in the junction
1723 // tree that contains "targets":
1724
1725 // 1/ we should check that all the nodes belong to the join tree
1726 for (const auto node: targets) {
1727 if (!_graph_.exists(node)) {
1729 "The variable " << this->MRF().variable(node).name() << "(" << node
1730 << ") does not belong to this optimized inference.")
1731 }
1732 }
1733
1734 // 2/ the clique created by the first eliminated node among target is the
1735 // one we are looking for
1736 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
1737 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
1738 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
1739 elim_order.insert(JT_elim_order[i], (int)i);
1740 NodeId first_eliminated_node = *(targets.begin());
1741 int elim_number = elim_order[first_eliminated_node];
1742 for (const auto node: targets) {
1743 if (elim_order[node] < elim_number) {
1744 elim_number = elim_order[node];
1745 first_eliminated_node = node;
1746 }
1747 }
1748
1749 clique_of_set = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
1750
1751
1752 // 3/ check that clique_of_set contains the all the nodes in the target
1753 const NodeSet& clique_nodes = _JT_->clique(clique_of_set);
1754 for (const auto node: targets) {
1755 if (!clique_nodes.contains(node)) {
1757 this->MRF().names(set) << "(" << set << ")"
1758 << " is not addressable in this optimized inference.")
1759 }
1760 }
1761
1762 // add the discovered clique to _joint_target_to_clique_
1763 _joint_target_to_clique_.insert(set, clique_of_set);
1764 }
1765
1766 // now perform a collect on the clique
1767 _collectMessage_(schedule, clique_of_set, clique_of_set);
1768
1769 // now we just need to create the product of the tensors of the clique
1770 // containing set with the messages received by this clique and
1771 // marginalize out all variables except set
1772 _ScheduleMultiDimSet_ pot_list;
1773 if (_clique_ss_tensor_[clique_of_set] != nullptr) {
1774 auto pot = _clique_ss_tensor_[clique_of_set];
1775 if (!schedule.existsScheduleMultiDim(pot->id())) schedule.emplaceScheduleMultiDim(*pot);
1776 pot_list.insert(_clique_ss_tensor_[clique_of_set]);
1777 }
1778
1779 // add the messages sent by adjacent nodes to targetClique
1780 for (const auto other: _JT_->neighbours(clique_of_set)) {
1781 const auto pot = _separator_tensors_[Arc(other, clique_of_set)];
1782 if (pot != nullptr) pot_list.insert(pot);
1783 }
1784
1785
1786 // get the set of variables that need be removed from the tensors
1787 const NodeSet& nodes = _JT_->clique(clique_of_set);
1788 gum::VariableSet del_vars(nodes.size());
1789 gum::VariableSet kept_vars(targets.size());
1790 const auto& mn = this->MRF();
1791 for (const auto node: nodes) {
1792 if (!targets.contains(node)) {
1793 del_vars.insert(&(mn.variable(node)));
1794 } else {
1795 kept_vars.insert(&(mn.variable(node)));
1796 }
1797 }
1798
1799 // pot_list now contains all the tensors to multiply and marginalize
1800 // => combine the messages
1801 const IScheduleMultiDim* new_pot = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
1802 scheduler.execute(schedule);
1803 ScheduleMultiDim< Tensor< GUM_SCALAR > >* resulting_pot
1804 = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1805 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(new_pot));
1806
1807 // if pot already existed, create a copy, so that we can put it into
1808 // the _target_posteriors_ property
1809 Tensor< GUM_SCALAR >* joint = nullptr;
1810 if (pot_list.exists(resulting_pot)) {
1811 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
1812 } else {
1813 joint = resulting_pot->exportMultiDim();
1814 }
1815
1816 // check that the joint posterior is different from a 0 vector: this would
1817 // indicate that some hard evidence are not compatible
1818 bool nonzero_found = false;
1819 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1820 if ((*joint)[inst]) {
1821 nonzero_found = true;
1822 break;
1823 }
1824 }
1825 if (!nonzero_found) {
1826 // remove joint from memory to avoid memory leaks
1827 delete joint;
1829 "some evidence entered into the Markov "
1830 "net are incompatible (their joint proba = 0)");
1831 }
1832
1833 return joint;
1834 }
1835
1837 template < GUM_Numeric GUM_SCALAR >
1838 Tensor< GUM_SCALAR >*
1839 ShaferShenoyMRFInference< GUM_SCALAR >::_unnormalizedJointPosterior_(const NodeSet& set) {
1840 // hard evidence do not belong to the join tree, so extract the nodes
1841 // from targets that are not hard evidence
1842 NodeSet targets = set, hard_ev_nodes;
1843 for (const auto node: this->hardEvidenceNodes()) {
1844 if (targets.contains(node)) {
1845 targets.erase(node);
1846 hard_ev_nodes.insert(node);
1847 }
1848 }
1849
1850 // if all the nodes have received hard evidence, then compute the
1851 // joint posterior directly by multiplying the hard evidence tensors
1852 const auto& evidence = this->evidence();
1853 if (targets.empty()) {
1854 if (set.size() == 1) {
1855 return new Tensor< GUM_SCALAR >(*evidence[*set.begin()]);
1856 } else {
1857 _TensorSet_ pot_list;
1858 for (const auto node: set) {
1859 pot_list.insert(evidence[node]);
1860 }
1861
1862 // combine all the tensors of the nodes in set
1863 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1864 const Tensor< GUM_SCALAR >* pot = fast_combination.execute(pot_list);
1865
1866 return const_cast< Tensor< GUM_SCALAR >* >(pot);
1867 }
1868 }
1869
1870
1871 // if we still need to perform some inference task, do it: so, first,
1872 // determine the clique on which we should perform collect to compute
1873 // the unnormalized joint posterior of a set of nodes containing "targets"
1874 NodeId clique_of_set;
1875 if (auto p_clique = _joint_target_to_clique_.tryGet(set)) {
1876 clique_of_set = *p_clique;
1877 } else {
1878 // here, the precise set of targets does not belong to the set of targets
1879 // defined by the user. So we will try to find a clique in the junction
1880 // tree that contains "targets":
1881
1882 // 1/ we should check that all the nodes belong to the join tree
1883 for (const auto node: targets) {
1884 if (!_graph_.exists(node)) {
1886 "The variable " << this->MRF().variable(node).name() << "(" << node
1887 << ") does not belong to this optimized inference.")
1888 }
1889 }
1890
1891 // 2/ the clique created by the first eliminated node among target is the
1892 // one we are looking for
1893 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
1894 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
1895 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
1896 elim_order.insert(JT_elim_order[i], (int)i);
1897 NodeId first_eliminated_node = *(targets.begin());
1898 int elim_number = elim_order[first_eliminated_node];
1899 for (const auto node: targets) {
1900 if (elim_order[node] < elim_number) {
1901 elim_number = elim_order[node];
1902 first_eliminated_node = node;
1903 }
1904 }
1905
1906 clique_of_set = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
1907
1908
1909 // 3/ check that clique_of_set contains the all the nodes in the target
1910 const NodeSet& clique_nodes = _JT_->clique(clique_of_set);
1911 for (const auto node: targets) {
1912 if (!clique_nodes.contains(node)) {
1913 GUM_ERROR(UndefinedElement, set << " is not a joint target")
1914 }
1915 }
1916
1917 // add the discovered clique to _joint_target_to_clique_
1918 _joint_target_to_clique_.insert(set, clique_of_set);
1919 }
1920
1921 // now perform a collect on the clique
1922 _collectMessage_(clique_of_set, clique_of_set);
1923
1924 // now we just need to create the product of the tensors of the clique
1925 // containing set with the messages received by this clique and
1926 // marginalize out all variables except set
1927 _ScheduleMultiDimSet_ pot_list;
1928 if (_clique_ss_tensor_[clique_of_set] != nullptr) {
1929 auto pot = _clique_ss_tensor_[clique_of_set];
1930 if (pot != nullptr) pot_list.insert(_clique_ss_tensor_[clique_of_set]);
1931 }
1932
1933 // add the messages sent by adjacent nodes to targetClique
1934 for (const auto other: _JT_->neighbours(clique_of_set)) {
1935 const auto pot = _separator_tensors_[Arc(other, clique_of_set)];
1936 if (pot != nullptr) pot_list.insert(pot);
1937 }
1938
1939
1940 // get the set of variables that need be removed from the tensors
1941 const NodeSet& nodes = _JT_->clique(clique_of_set);
1942 gum::VariableSet del_vars(nodes.size());
1943 gum::VariableSet kept_vars(targets.size());
1944 const auto& mn = this->MRF();
1945 for (const auto node: nodes) {
1946 if (!targets.contains(node)) {
1947 del_vars.insert(&(mn.variable(node)));
1948 } else {
1949 kept_vars.insert(&(mn.variable(node)));
1950 }
1951 }
1952
1953 // pot_list now contains all the tensors to multiply and marginalize
1954 // => combine the messages
1955 const IScheduleMultiDim* new_pot = _marginalizeOut_(pot_list, del_vars, kept_vars);
1956 ScheduleMultiDim< Tensor< GUM_SCALAR > >* resulting_pot
1957 = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1958 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(new_pot));
1959
1960 // if pot already existed, create a copy, so that we can put it into
1961 // the _target_posteriors_ property
1962 Tensor< GUM_SCALAR >* joint = nullptr;
1963 if (pot_list.exists(resulting_pot)) {
1964 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
1965 } else {
1966 joint = resulting_pot->exportMultiDim();
1967 delete new_pot;
1968 }
1969
1970 // check that the joint posterior is different from a 0 vector: this would
1971 // indicate that some hard evidence are not compatible
1972 bool nonzero_found = false;
1973 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1974 if ((*joint)[inst]) {
1975 nonzero_found = true;
1976 break;
1977 }
1978 }
1979 if (!nonzero_found) {
1980 // remove joint from memory to avoid memory leaks
1981 delete joint;
1983 "some evidence entered into the Markov "
1984 "net are incompatible (their joint proba = 0)");
1985 }
1986
1987 return joint;
1988 }
1989
1991 template < GUM_Numeric GUM_SCALAR >
1992 const Tensor< GUM_SCALAR >&
1993 ShaferShenoyMRFInference< GUM_SCALAR >::jointPosterior_(const NodeSet& set) {
1994 // check if we have already computed the posterior
1995 if (auto p = _joint_target_posteriors_.tryGet(set)) { return *(*p); }
1996
1997 // compute the joint posterior and normalize
1998 auto joint = unnormalizedJointPosterior_(set);
1999 joint->normalize();
2000 _joint_target_posteriors_.insert(set, joint);
2001
2002 return *joint;
2003 }
2004
2006 template < GUM_Numeric GUM_SCALAR >
2007 const Tensor< GUM_SCALAR >&
2008 ShaferShenoyMRFInference< GUM_SCALAR >::jointPosterior_(const NodeSet& wanted_target,
2009 const NodeSet& declared_target) {
2010 // check if we have already computed the posterior of wanted_target
2011 if (auto p = _joint_target_posteriors_.tryGet(wanted_target)) return *(*p);
2012
2013 // here, we will have to compute the posterior of declared_target and
2014 // marginalize out all the variables that do not belong to wanted_target
2015
2016 // check if we have already computed the posterior of declared_target
2017 if (!_joint_target_posteriors_.exists(declared_target)) {
2018 return jointPosterior_(declared_target);
2019 }
2020
2021 // marginalize out all the variables that do not belong to wanted_target
2022 const auto& mn = this->MRF();
2023 gum::VariableSet del_vars;
2024 for (const auto node: declared_target)
2025 if (!wanted_target.contains(node)) del_vars.insert(&(mn.variable(node)));
2026 auto pot
2027 = new Tensor< GUM_SCALAR >(_joint_target_posteriors_[declared_target]->sumOut(del_vars));
2028
2029 // save the result into the cache
2030 _joint_target_posteriors_.insert(wanted_target, pot);
2031
2032 return *pot;
2033 }
2034
2035 template < GUM_Numeric GUM_SCALAR >
2036 GUM_SCALAR ShaferShenoyMRFInference< GUM_SCALAR >::evidenceProbability() {
2037 // perform inference in each connected component
2038 this->makeInference();
2039
2040 // for each connected component, select a variable X and compute the
2041 // joint probability of X and evidence e. Then marginalize-out X to get
2042 // p(e) in this connected component. Finally, multiply all the p(e) that
2043 // we got and the elements in _constants_. The result is the probability
2044 // of evidence
2045
2046 GUM_SCALAR prob_ev = 1;
2047 for (const auto root: _roots_) {
2048 // get a node in the clique
2049 NodeId node = *(_JT_->clique(root).begin());
2050 Tensor< GUM_SCALAR >* tmp = unnormalizedJointPosterior_(node);
2051 prob_ev *= tmp->sum();
2052 delete tmp;
2053 }
2054
2055 for (const auto& projected_cpt: _constants_)
2056 prob_ev *= projected_cpt.second;
2057
2058 return prob_ev;
2059 }
2060
2061 template < GUM_Numeric GUM_SCALAR >
2062 bool ShaferShenoyMRFInference< GUM_SCALAR >::isExactJointComputable_(const NodeSet& vars) {
2063 if (JointTargetedMRFInference< GUM_SCALAR >::isExactJointComputable_(vars)) return true;
2064
2065 this->prepareInference();
2066
2067 for (const auto& node: this->_JT_->nodes()) {
2068 const auto clique = _JT_->clique(node);
2069 if (vars == clique) return true;
2070 }
2071 return false;
2072 }
2073
2074 template < GUM_Numeric GUM_SCALAR >
2075 NodeSet ShaferShenoyMRFInference< GUM_SCALAR >::superForJointComputable_(const NodeSet& vars) {
2076 const auto superset = JointTargetedMRFInference< GUM_SCALAR >::superForJointComputable_(vars);
2077 if (!superset.empty()) return superset;
2078
2079 this->prepareInference();
2080
2081 for (const auto& node: _JT_->nodes()) {
2082 const auto clique = _JT_->clique(node);
2083 if (vars.isStrictSubsetOf(clique)) return clique;
2084 }
2085
2086
2087 return NodeSet();
2088 }
2089
2090 // the function used to combine two tables
2091 template < GUM_Numeric GUM_SCALAR >
2092 static Tensor< GUM_SCALAR > SSNewMNmultiTensor(const Tensor< GUM_SCALAR >& t1,
2093 const Tensor< GUM_SCALAR >& t2) {
2094 return t1 * t2;
2095 }
2096
2097 // the function used to project (marginalize) a table
2098 template < GUM_Numeric GUM_SCALAR >
2099 static Tensor< GUM_SCALAR > SSNewMNprojTensor(const Tensor< GUM_SCALAR >& t1,
2100 const gum::VariableSet& del_vars) {
2101 return t1.sumOut(del_vars);
2102 }
2103
2104 template < GUM_Numeric GUM_SCALAR >
2105 void ShaferShenoyMRFInference< GUM_SCALAR >::onStateChanged_() {}
2106
2107} /* namespace gum */
2108
2109#endif // DOXYGEN_SHOULD_SKIP_THIS
Implementation of Shafer-Shenoy's algorithm for inference in Markov random fields.
An algorithm for converting a join tree into a binary join tree.
Exception : a similar element already exists.
<agrum/MRF/inference/evidenceMRFInference.h>
Exception : fatal (unknown ?) error.
Class representing the minimal interface for Markov random field.
Exception : several evidence are incompatible together (proba=0).
<agrum/MRF/inference/jointTargetedMRFInference.h>
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:510
ShaferShenoyMRFInference(const IMarkovRandomField< GUM_SCALAR > *MN, bool use_binary_join_tree=true)
default constructor
Exception : a looked-for element could not be found.
#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
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...