aGrUM 3.1.1
a C++ library for (probabilistic) graphical models
variableElimination_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/variableElimination.h> // to ease IDE parser
53#ifndef DOXYGEN_SHOULD_SKIP_THIS
54# include <algorithm>
55
64
65namespace gum {
66
67
68 // default constructor
69 template < GUM_Numeric GUM_SCALAR >
71 RelevantTensorsFinderType relevant_type,
72 FindBarrenNodesType barren_type) :
73 JointTargetedInference< GUM_SCALAR >(BN) {
74 // sets the relevant tensor and the barren nodes finding algorithm
75 _findRelevantTensors_
76 = &VariableElimination< 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(VariableElimination);
85 }
86
87 // destructor
88 template < GUM_Numeric GUM_SCALAR >
89 VariableElimination< GUM_SCALAR >::~VariableElimination() {
90 // remove the junction tree and the triangulation algorithm
91 if (_JT_ != nullptr) delete _JT_;
92 delete _triangulation_;
93 if (_target_posterior_ != nullptr) delete _target_posterior_;
94
95 // for debugging purposes
96 GUM_DESTRUCTOR(VariableElimination);
97 }
98
100 template < GUM_Numeric GUM_SCALAR >
101 void VariableElimination< GUM_SCALAR >::setTriangulation(const Triangulation& new_triangulation) {
102 delete _triangulation_;
103 _triangulation_ = new_triangulation.newFactory();
104 }
105
107 template < GUM_Numeric GUM_SCALAR >
108 const JunctionTree* VariableElimination< GUM_SCALAR >::junctionTree(NodeId id) {
109 _createNewJT_(NodeSet{id});
110
111 return _JT_;
112 }
113
115 template < GUM_Numeric GUM_SCALAR >
116 void VariableElimination< GUM_SCALAR >::setRelevantTensorsFinderType(
117 RelevantTensorsFinderType type) {
118 if (type != _find_relevant_tensor_type_) {
119 switch (type) {
120 case RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS :
121 _findRelevantTensors_
122 = &VariableElimination< GUM_SCALAR >::_findRelevantTensorsWithdSeparation2_;
123 break;
124
125 case RelevantTensorsFinderType::DSEP_BAYESBALL_NODES :
126 _findRelevantTensors_
127 = &VariableElimination< GUM_SCALAR >::_findRelevantTensorsWithdSeparation_;
128 break;
129
130 case RelevantTensorsFinderType::DSEP_KOLLER_FRIEDMAN_2009 :
131 _findRelevantTensors_
132 = &VariableElimination< GUM_SCALAR >::_findRelevantTensorsWithdSeparation3_;
133 break;
134
135 case RelevantTensorsFinderType::FIND_ALL :
136 _findRelevantTensors_ = &VariableElimination< GUM_SCALAR >::_findRelevantTensorsGetAll_;
137 break;
138
139 default :
141 "setRelevantTensorsFinderType for type " << (unsigned int)type
142 << " is not implemented yet");
143 }
144
145 _find_relevant_tensor_type_ = type;
146 }
147 }
148
150 template < GUM_Numeric GUM_SCALAR >
151 void VariableElimination< GUM_SCALAR >::_setProjectionFunction_(
152 Tensor< GUM_SCALAR > (*proj)(const Tensor< GUM_SCALAR >&, const gum::VariableSet&)) {
153 _projection_op_ = proj;
154 }
155
157 template < GUM_Numeric GUM_SCALAR >
158 void VariableElimination< GUM_SCALAR >::_setCombinationFunction_(
159 Tensor< GUM_SCALAR > (*comb)(const Tensor< GUM_SCALAR >&, const Tensor< GUM_SCALAR >&)) {
160 _combination_op_ = comb;
161 }
162
164 template < GUM_Numeric GUM_SCALAR >
165 void VariableElimination< GUM_SCALAR >::setFindBarrenNodesType(FindBarrenNodesType type) {
166 if (type != _barren_nodes_type_) {
167 // WARNING: if a new type is added here, method _createJT_ should certainly
168 // be updated as well, in particular its step 2.
169 switch (type) {
170 case FindBarrenNodesType::FIND_BARREN_NODES :
171 case FindBarrenNodesType::FIND_NO_BARREN_NODES : break;
172
173 default :
175 "setFindBarrenNodesType for type " << (unsigned int)type
176 << " is not implemented yet");
177 }
178
179 _barren_nodes_type_ = type;
180 }
181 }
182
184 template < GUM_Numeric GUM_SCALAR >
185 void VariableElimination< GUM_SCALAR >::onEvidenceAdded_(const NodeId, bool) {}
186
188 template < GUM_Numeric GUM_SCALAR >
189 void VariableElimination< GUM_SCALAR >::onEvidenceErased_(const NodeId, bool) {}
190
192 template < GUM_Numeric GUM_SCALAR >
193 void VariableElimination< GUM_SCALAR >::onAllEvidenceErased_(bool) {}
194
196 template < GUM_Numeric GUM_SCALAR >
197 void VariableElimination< GUM_SCALAR >::onEvidenceChanged_(const NodeId, bool) {}
198
200 template < GUM_Numeric GUM_SCALAR >
201 void VariableElimination< GUM_SCALAR >::onMarginalTargetAdded_(const NodeId) {}
202
204 template < GUM_Numeric GUM_SCALAR >
205 void VariableElimination< GUM_SCALAR >::onMarginalTargetErased_(const NodeId) {}
206
208 template < GUM_Numeric GUM_SCALAR >
209 void VariableElimination< GUM_SCALAR >::onModelChanged_(const GraphicalModel* bn) {}
210
212 template < GUM_Numeric GUM_SCALAR >
213 void VariableElimination< GUM_SCALAR >::onJointTargetAdded_(const NodeSet&) {}
214
216 template < GUM_Numeric GUM_SCALAR >
217 void VariableElimination< GUM_SCALAR >::onJointTargetErased_(const NodeSet&) {}
218
220 template < GUM_Numeric GUM_SCALAR >
221 void VariableElimination< GUM_SCALAR >::onAllMarginalTargetsAdded_() {}
222
224 template < GUM_Numeric GUM_SCALAR >
225 void VariableElimination< GUM_SCALAR >::onAllMarginalTargetsErased_() {}
226
228 template < GUM_Numeric GUM_SCALAR >
229 void VariableElimination< GUM_SCALAR >::onAllJointTargetsErased_() {}
230
232 template < GUM_Numeric GUM_SCALAR >
233 void VariableElimination< GUM_SCALAR >::onAllTargetsErased_() {}
234
236 template < GUM_Numeric GUM_SCALAR >
237 void VariableElimination< GUM_SCALAR >::_createNewJT_(const NodeSet& targets) {
238 // to create the JT, we first create the moral graph of the BN in the
239 // following way in order to take into account the barren nodes and the
240 // nodes that received evidence:
241 // 1/ we create an undirected graph containing only the nodes and no edge
242 // 2/ if we take into account barren nodes, remove them from the graph
243 // 3/ if we take d-separation into account, remove the d-separated nodes
244 // 4/ add edges so that each node and its parents in the BN form a clique
245 // 5/ add edges so that joint targets form a clique of the moral graph
246 // 6/ remove the nodes that received hard evidence (by step 4/, their
247 // parents are linked by edges, which is necessary for inference)
248 //
249 // At the end of step 6/, we have our moral graph and we can triangulate it
250 // to get the new junction tree
251
252 // 1/ create an undirected graph containing only the nodes and no edge
253 const auto& bn = this->BN();
254 _graph_.clear();
255 for (const auto node: bn.internalDag())
256 _graph_.addNodeWithId(node);
257
258 // 2/ if we wish to exploit barren nodes, we shall remove them from the
259 // BN. To do so: we identify all the nodes that are not targets and have
260 // received no evidence and such that their descendants are neither
261 // targets nor evidence nodes. Such nodes can be safely discarded from
262 // the BN without altering the inference output
263 if (_barren_nodes_type_ == FindBarrenNodesType::FIND_BARREN_NODES) {
264 // check that all the nodes are not targets, otherwise, there is no
265 // barren node
266 if (targets.size() != bn.size()) {
267 BarrenNodesFinder finder(&bn.internalDag());
268 finder.setTargets(&targets);
269
270 NodeSet evidence_nodes(this->evidence().size());
271 for (const auto& pair: this->evidence()) {
272 evidence_nodes.insert(pair.first);
273 }
274 finder.setEvidence(&evidence_nodes);
275
276 NodeSet barren_nodes = finder.barrenNodes();
277
278 // remove the barren nodes from the moral graph
279 for (const auto node: barren_nodes) {
280 _graph_.eraseNode(node);
281 }
282 }
283 }
284
285 // 3/ if we wish to exploit d-separation, remove all the nodes that are
286 // d-separated from our targets
287 {
288 NodeSet requisite_nodes;
289 bool dsep_analysis = false;
290 switch (_find_relevant_tensor_type_) {
291 case RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS :
292 case RelevantTensorsFinderType::DSEP_BAYESBALL_NODES : {
293 BayesBall::requisiteNodes(bn.internalDag(),
294 targets,
295 this->hardEvidenceNodes(),
296 this->softEvidenceNodes(),
297 requisite_nodes);
298 dsep_analysis = true;
299 } break;
300
301 case RelevantTensorsFinderType::DSEP_KOLLER_FRIEDMAN_2009 : {
302 dSeparationAlgorithm dsep;
303 dsep.requisiteNodes(bn.internalDag(),
304 targets,
305 this->hardEvidenceNodes(),
306 this->softEvidenceNodes(),
307 requisite_nodes);
308 dsep_analysis = true;
309 } break;
310
311 case RelevantTensorsFinderType::FIND_ALL : break;
312
313 default : GUM_ERROR(FatalError, "not implemented yet")
314 }
315
316 // remove all the nodes that are not requisite
317 if (dsep_analysis) {
318 for (auto iter = _graph_.beginSafe(); iter != _graph_.endSafe(); ++iter) {
319 if (!requisite_nodes.contains(*iter) && !this->hardEvidenceNodes().contains(*iter)) {
320 _graph_.eraseNode(*iter);
321 }
322 }
323 }
324 }
325
326 // 4/ add edges so that each node and its parents in the BN form a clique
327 for (const auto node: _graph_) {
328 const NodeSet& parents = bn.parents(node);
329 for (auto iter1 = parents.cbegin(); iter1 != parents.cend(); ++iter1) {
330 // before adding an edge between node and its parent, check that the
331 // parent belong to the graph. Actually, when d-separated nodes are
332 // removed, it may be the case that the parents of hard evidence nodes
333 // are removed. But the latter still exist in the graph.
334 if (_graph_.existsNode(*iter1)) {
335 _graph_.addEdge(*iter1, node);
336
337 auto iter2 = iter1;
338 for (++iter2; iter2 != parents.cend(); ++iter2) {
339 // before adding an edge, check that both extremities belong to
340 // the graph. Actually, when d-separated nodes are removed, it may
341 // be the case that the parents of hard evidence nodes are removed.
342 // But the latter still exist in the graph.
343 if (_graph_.existsNode(*iter2)) _graph_.addEdge(*iter1, *iter2);
344 }
345 }
346 }
347 }
348
349 // 5/ if targets contains several nodes, we shall add new edges into the
350 // moral graph in order to ensure that there exists a clique containing
351 // their joint distribution
352 for (auto iter1 = targets.cbegin(); iter1 != targets.cend(); ++iter1) {
353 auto iter2 = iter1;
354 for (++iter2; iter2 != targets.cend(); ++iter2) {
355 _graph_.addEdge(*iter1, *iter2);
356 }
357 }
358
359 // 6/ remove all the nodes that received hard evidence
360 const auto& hard_ev_nodes = this->hardEvidenceNodes();
361 for (const auto node: hard_ev_nodes) {
362 _graph_.eraseNode(node);
363 }
364
365
366 // now, we can compute the new junction tree.
367 if (_JT_ != nullptr) delete _JT_;
368 _triangulation_->setGraph(&_graph_, &(this->domainSizes()));
369 const JunctionTree& triang_jt = _triangulation_->junctionTree();
370 _JT_ = new CliqueGraph(triang_jt);
371
372 // indicate, for each node of the moral graph, a clique in _JT_ that can
373 // contain its conditional probability table
374 _node_to_clique_.clear();
375 _clique_to_nodes_.clear();
376 NodeSet emptyset(_JT_->size());
377 for (auto clique: *_JT_)
378 _clique_to_nodes_.insert(clique, emptyset);
379 const std::vector< NodeId >& JT_elim_order = _triangulation_->eliminationOrder();
380 NodeProperty< int > elim_order(Size(JT_elim_order.size()));
381 for (std::size_t i = std::size_t(0), size = JT_elim_order.size(); i < size; ++i)
382 elim_order.insert(JT_elim_order[i], (int)i);
383 const DAG& dag = bn.internalDag();
384
385 for (const auto node: _graph_) {
386 // get the variables in the tensor of node (and its parents)
387 NodeId first_eliminated_node = node;
388 int elim_number = elim_order[first_eliminated_node];
389
390 for (const auto parent: dag.parents(node)) {
391 if (_graph_.existsNode(parent) && (elim_order[parent] < elim_number)) {
392 elim_number = elim_order[parent];
393 first_eliminated_node = parent;
394 }
395 }
396
397 // first_eliminated_node contains the first var (node or one of its
398 // parents) eliminated => the clique created during its elimination
399 // contains node and all of its parents => it can contain the tensor
400 // assigned to the node in the BN
401 NodeId clique = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
402 _node_to_clique_.insert(node, clique);
403 _clique_to_nodes_[clique].insert(node);
404 }
405
406 // do the same for the nodes that received hard evidence. Here, we only store
407 // the nodes for which at least one parent belongs to _graph_ (otherwise
408 // their CPT is just a constant real number).
409 for (const auto node: hard_ev_nodes) {
410 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
411 int elim_number = std::numeric_limits< int >::max();
412
413 for (const auto parent: dag.parents(node)) {
414 if (_graph_.exists(parent) && (elim_order[parent] < elim_number)) {
415 elim_number = elim_order[parent];
416 first_eliminated_node = parent;
417 }
418 }
419
420 // first_eliminated_node contains the first var (node or one of its
421 // parents) eliminated => the clique created during its elimination
422 // contains node and all of its parents => it can contain the tensor
423 // assigned to the node in the BN
424 if (elim_number != std::numeric_limits< int >::max()) {
425 NodeId clique = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
426 _node_to_clique_.insert(node, clique);
427 _clique_to_nodes_[clique].insert(node);
428 }
429 }
430
431
432 // indicate a clique that contains all the nodes of targets
433 _targets2clique_ = std::numeric_limits< NodeId >::max();
434 {
435 // note that we remove from set all the nodes that received hard evidence
436 // (since they do not belong to the join tree)
437 NodeId first_eliminated_node = std::numeric_limits< NodeId >::max();
438 int elim_number = std::numeric_limits< int >::max();
439
440 for (const auto node: targets) {
441 if (!hard_ev_nodes.contains(node) && (elim_order[node] < elim_number)) {
442 elim_number = elim_order[node];
443 first_eliminated_node = node;
444 }
445 }
446
447 if (elim_number != std::numeric_limits< int >::max()) {
448 _targets2clique_ = _triangulation_->createdJunctionTreeClique(first_eliminated_node);
449 }
450 }
451 }
452
454 template < GUM_Numeric GUM_SCALAR >
455 void VariableElimination< GUM_SCALAR >::updateOutdatedStructure_() {}
456
459 template < GUM_Numeric GUM_SCALAR >
460 void VariableElimination< GUM_SCALAR >::updateOutdatedTensors_() {}
461
462 // find the tensors d-connected to a set of variables
463 template < GUM_Numeric GUM_SCALAR >
464 void VariableElimination< GUM_SCALAR >::_findRelevantTensorsGetAll_(
465 Set< const IScheduleMultiDim* >& pot_list,
466 gum::VariableSet& kept_vars) {}
467
468 // find the tensors d-connected to a set of variables
469 template < GUM_Numeric GUM_SCALAR >
470 void VariableElimination< GUM_SCALAR >::_findRelevantTensorsWithdSeparation_(
471 Set< const IScheduleMultiDim* >& pot_list,
472 gum::VariableSet& kept_vars) {
473 // find the node ids of the kept variables
474 NodeSet kept_ids(kept_vars.size());
475 const auto& bn = this->BN();
476 for (const auto var: kept_vars) {
477 kept_ids.insert(bn.nodeId(*var));
478 }
479
480 // determine the set of tensors d-connected with the kept variables
481 NodeSet requisite_nodes;
482 BayesBall::requisiteNodes(bn.internalDag(),
483 kept_ids,
484 this->hardEvidenceNodes(),
485 this->softEvidenceNodes(),
486 requisite_nodes);
487 for (auto iter = pot_list.beginSafe(); iter != pot_list.endSafe(); ++iter) {
488 const Sequence< const DiscreteVariable* >& vars = (*iter)->variablesSequence();
489 bool found = false;
490 for (const auto var: vars) {
491 if (requisite_nodes.exists(bn.nodeId(*var))) {
492 found = true;
493 break;
494 }
495 }
496
497 if (!found) { pot_list.erase(iter); }
498 }
499 }
500
501 // find the tensors d-connected to a set of variables
502 template < GUM_Numeric GUM_SCALAR >
503 void VariableElimination< GUM_SCALAR >::_findRelevantTensorsWithdSeparation2_(
504 Set< const IScheduleMultiDim* >& pot_list,
505 gum::VariableSet& kept_vars) {
506 // find the node ids of the kept variables
507 NodeSet kept_ids(kept_vars.size());
508 const auto& bn = this->BN();
509 for (const auto var: kept_vars) {
510 kept_ids.insert(bn.nodeId(*var));
511 }
512
513 // determine the set of tensors d-connected with the kept variables
514 BayesBall::relevantTensors(bn,
515 kept_ids,
516 this->hardEvidenceNodes(),
517 this->softEvidenceNodes(),
518 pot_list);
519 }
520
521 // find the tensors d-connected to a set of variables
522 template < GUM_Numeric GUM_SCALAR >
523 void VariableElimination< GUM_SCALAR >::_findRelevantTensorsWithdSeparation3_(
524 Set< const IScheduleMultiDim* >& pot_list,
525 gum::VariableSet& kept_vars) {
526 // find the node ids of the kept variables
527 NodeSet kept_ids(kept_vars.size());
528 const auto& bn = this->BN();
529 for (const auto var: kept_vars) {
530 kept_ids.insert(bn.nodeId(*var));
531 }
532
533 // determine the set of tensors d-connected with the kept variables
534 dSeparationAlgorithm dsep;
535 dsep.relevantTensors(bn,
536 kept_ids,
537 this->hardEvidenceNodes(),
538 this->softEvidenceNodes(),
539 pot_list);
540 }
541
542 // find the tensors d-connected to a set of variables
543 template < GUM_Numeric GUM_SCALAR >
544 void VariableElimination< GUM_SCALAR >::_findRelevantTensorsXX_(
545 Set< const IScheduleMultiDim* >& pot_list,
546 gum::VariableSet& kept_vars) {
547 switch (_find_relevant_tensor_type_) {
548 case RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS :
549 _findRelevantTensorsWithdSeparation2_(pot_list, kept_vars);
550 break;
551
552 case RelevantTensorsFinderType::DSEP_BAYESBALL_NODES :
553 _findRelevantTensorsWithdSeparation_(pot_list, kept_vars);
554 break;
555
556 case RelevantTensorsFinderType::DSEP_KOLLER_FRIEDMAN_2009 :
557 _findRelevantTensorsWithdSeparation3_(pot_list, kept_vars);
558 break;
559
560 case RelevantTensorsFinderType::FIND_ALL :
561 _findRelevantTensorsGetAll_(pot_list, kept_vars);
562 break;
563
564 default : GUM_ERROR(FatalError, "not implemented yet")
565 }
566 }
567
568 // remove barren variables using schedules
569 template < GUM_Numeric GUM_SCALAR >
570 Set< const IScheduleMultiDim* >
571 VariableElimination< GUM_SCALAR >::_removeBarrenVariables_(Schedule& schedule,
572 _ScheduleMultiDimSet_& pot_list,
573 gum::VariableSet& del_vars) {
574 // remove from del_vars the variables that received some evidence:
575 // only those that did not receive evidence can be barren variables
576 gum::VariableSet the_del_vars = del_vars;
577 for (auto iter = the_del_vars.beginSafe(); iter != the_del_vars.endSafe(); ++iter) {
578 NodeId id = this->BN().nodeId(**iter);
579 if (this->hardEvidenceNodes().exists(id) || this->softEvidenceNodes().exists(id)) {
580 the_del_vars.erase(iter);
581 }
582 }
583
584 // assign to each random variable the set of tensors that contain it
585 HashTable< const DiscreteVariable*, _ScheduleMultiDimSet_ > var2pots(the_del_vars.size());
586 _ScheduleMultiDimSet_ empty_pot_set;
587 for (const auto pot: pot_list) {
588 const auto& vars = pot->variablesSequence();
589 for (const auto var: vars) {
590 if (the_del_vars.exists(var)) {
591 if (!var2pots.exists(var)) { var2pots.insert(var, empty_pot_set); }
592 var2pots[var].insert(pot);
593 }
594 }
595 }
596
597 // each variable with only one tensor is necessarily a barren variable
598 // assign to each tensor with barren nodes its set of barren variables
599 HashTable< const IScheduleMultiDim*, gum::VariableSet > pot2barren_var;
600 gum::VariableSet empty_var_set;
601 for (const auto& elt: var2pots) {
602 if (elt.second.size() == 1) { // here we have a barren variable
603 const IScheduleMultiDim* pot = *(elt.second.begin());
604 if (!pot2barren_var.exists(pot)) { pot2barren_var.insert(pot, empty_var_set); }
605 pot2barren_var[pot].insert(elt.first); // insert the barren variable
606 }
607 }
608
609 // for each tensor with barren variables, marginalize them.
610 // if the tensor has only barren variables, simply remove them from the
611 // set of tensors, else just project the tensor
612 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
613 _ScheduleMultiDimSet_ projected_pots;
614 for (const auto& elt: pot2barren_var) {
615 // remove the current tensor from pot_list as, anyway, we will change it
616 const IScheduleMultiDim* pot = elt.first;
617 pot_list.erase(pot);
618
619 // check whether we need to add a projected new tensor or not (i.e.,
620 // whether there exist non-barren variables or not)
621 if (pot->variablesSequence().size() != elt.second.size()) {
622 const IScheduleMultiDim* new_pot = projector.schedule(schedule, pot, elt.second);
623 // here, there is no need to enforce that new_pot is persistent since,
624 // if this is needed, the function that called _removeBarrenVariables_ will
625 // do it
626 pot_list.insert(new_pot);
627 projected_pots.insert(new_pot);
628 }
629 }
630
631 return projected_pots;
632 }
633
634 // remove barren variables directly without schedules
635 template < GUM_Numeric GUM_SCALAR >
636 Set< const Tensor< GUM_SCALAR >* >
637 VariableElimination< GUM_SCALAR >::_removeBarrenVariables_(_TensorSet_& pot_list,
638 gum::VariableSet& del_vars) {
639 // remove from del_vars the variables that received some evidence:
640 // only those that did not receive evidence can be barren variables
641 gum::VariableSet the_del_vars = del_vars;
642 for (auto iter = the_del_vars.beginSafe(); iter != the_del_vars.endSafe(); ++iter) {
643 NodeId id = this->BN().nodeId(**iter);
644 if (this->hardEvidenceNodes().exists(id) || this->softEvidenceNodes().exists(id)) {
645 the_del_vars.erase(iter);
646 }
647 }
648
649 // assign to each random variable the set of tensors that contain it
650 HashTable< const DiscreteVariable*, _TensorSet_ > var2pots;
651 _TensorSet_ empty_pot_set;
652 for (const auto pot: pot_list) {
653 const Sequence< const DiscreteVariable* >& vars = pot->variablesSequence();
654 for (const auto var: vars) {
655 if (the_del_vars.exists(var)) {
656 if (!var2pots.exists(var)) { var2pots.insert(var, empty_pot_set); }
657 var2pots[var].insert(pot);
658 }
659 }
660 }
661
662 // each variable with only one tensor is a barren variable
663 // assign to each tensor with barren nodes its set of barren variables
664 HashTable< const Tensor< GUM_SCALAR >*, gum::VariableSet > pot2barren_var;
665 gum::VariableSet empty_var_set;
666 for (const auto& elt: var2pots) {
667 if (elt.second.size() == 1) { // here we have a barren variable
668 const Tensor< GUM_SCALAR >* pot = *(elt.second.begin());
669 if (!pot2barren_var.exists(pot)) { pot2barren_var.insert(pot, empty_var_set); }
670 pot2barren_var[pot].insert(elt.first); // insert the barren variable
671 }
672 }
673
674 // for each tensor with barren variables, marginalize them.
675 // if the tensor has only barren variables, simply remove them from the
676 // set of tensors, else just project the tensor
677 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
678 _TensorSet_ projected_pots;
679 for (const auto& elt: pot2barren_var) {
680 // remove the current tensor from pot_list as, anyway, we will change it
681 const Tensor< GUM_SCALAR >* pot = elt.first;
682 pot_list.erase(pot);
683
684 // check whether we need to add a projected new tensor or not (i.e.,
685 // whether there exist non-barren variables or not)
686 if (pot->variablesSequence().size() != elt.second.size()) {
687 const Tensor< GUM_SCALAR >* new_pot = projector.execute(*pot, elt.second);
688 pot_list.insert(new_pot);
689 projected_pots.insert(new_pot);
690 }
691 }
692
693 return projected_pots;
694 }
695
696 // performs the collect phase of Variable Elimination
697 template < GUM_Numeric GUM_SCALAR >
698 Set< const IScheduleMultiDim* >
699 VariableElimination< GUM_SCALAR >::_collectMessage_(Schedule& schedule,
700 NodeId id,
701 NodeId from) {
702 // collect messages from all the neighbors
703 _ScheduleMultiDimSet_ collected_messages;
704 for (const auto other: _JT_->neighbours(id)) {
705 if (other != from) {
706 _ScheduleMultiDimSet_ message(_collectMessage_(schedule, other, id));
707 collected_messages += message;
708 }
709 }
710
711 // combine the collect messages with those of id's clique
712 return _produceMessage_(schedule, id, from, std::move(collected_messages));
713 }
714
715 // performs the collect phase of Variable Elimination
716 template < GUM_Numeric GUM_SCALAR >
717 std::pair< Set< const Tensor< GUM_SCALAR >* >, Set< const Tensor< GUM_SCALAR >* > >
718 VariableElimination< GUM_SCALAR >::_collectMessage_(NodeId id, NodeId from) {
719 // collect messages from all the neighbors
720 std::pair< _TensorSet_, _TensorSet_ > collected_messages;
721 for (const auto other: _JT_->neighbours(id)) {
722 if (other != from) {
723 std::pair< _TensorSet_, _TensorSet_ > message(_collectMessage_(other, id));
724 collected_messages.first += message.first;
725 collected_messages.second += message.second;
726 }
727 }
728
729 // combine the collect messages with those of id's clique
730 return _produceMessage_(id, from, std::move(collected_messages));
731 }
732
733 // get the CPT + evidence of a node projected w.r.t. hard evidence
734 template < GUM_Numeric GUM_SCALAR >
735 Set< const IScheduleMultiDim* >
736 VariableElimination< GUM_SCALAR >::_NodeTensors_(Schedule& schedule, NodeId node) {
737 _ScheduleMultiDimSet_ res;
738 const auto& bn = this->BN();
739
740 // get the CPT of the node
741 // Beware: all the tensors that are defined over some nodes that
742 // received hard evidence must be projected so that these nodes are
743 // removed from the tensor.
744 // Also beware that the CPT of a hard evidence node may be defined over
745 // parents that do not belong to _graph_ and that are not hard evidence.
746 // In this case, those parents have been removed by d-separation and it is
747 // easy to show that, in this case, all the parents have been removed, so
748 // that the CPT does not need to be taken into account
749 const auto& evidence = this->evidence();
750 const auto& hard_evidence = this->hardEvidence();
751 const auto& hard_ev_nodes = this->hardEvidenceNodes();
752 if (_graph_.exists(node) || hard_ev_nodes.contains(node)) {
753 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node);
754 const auto& variables = cpt.variablesSequence();
755
756 // check if the parents of a hard evidence node do not belong to _graph_
757 // and are not themselves hard evidence. In this case, discard the CPT as
758 // it is useless for inference (see the above comment)
759 if (hard_ev_nodes.contains(node)) {
760 for (const auto var: variables) {
761 NodeId xnode = bn.nodeId(*var);
762 if (!hard_ev_nodes.contains(xnode) && !_graph_.existsNode(xnode)) return res;
763 }
764 }
765
766 // get the list of nodes with hard evidence in cpt
767 NodeSet hard_nodes(variables.size());
768 for (const auto var: variables) {
769 NodeId xnode = bn.nodeId(*var);
770 if (hard_ev_nodes.contains(xnode)) hard_nodes.insert(xnode);
771 }
772
773 // if hard_nodes contains hard evidence nodes, perform a projection
774 // and insert the result into the appropriate clique, else insert
775 // directly cpt into the clique
776 if (hard_nodes.empty()) {
777 const IScheduleMultiDim* sched_cpt
778 = schedule.insertTable< Tensor< GUM_SCALAR > >(cpt, false);
779 res.insert(sched_cpt);
780 } else {
781 // marginalize out the hard evidence nodes: if the cpt is defined
782 // only over nodes that received hard evidence, do not consider it
783 // as a tensor anymore
784 if (hard_nodes.size() != variables.size()) {
785 // perform the projection with a combine and project instance
786 gum::VariableSet hard_variables;
787 _ScheduleMultiDimSet_ marg_cpt_set(1 + hard_nodes.size());
788 const IScheduleMultiDim* sched_cpt
789 = schedule.insertTable< Tensor< GUM_SCALAR > >(cpt, false);
790 marg_cpt_set.insert(sched_cpt);
791
792 for (const auto xnode: hard_nodes) {
793 const IScheduleMultiDim* pot
794 = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[xnode], false);
795 marg_cpt_set.insert(pot);
796 hard_variables.insert(&(bn.variable(xnode)));
797 }
798
799 // perform the combination of those tensors and their projection
800 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
801 _combination_op_,
802 _projection_op_);
803 _ScheduleMultiDimSet_ new_cpt_list
804 = combine_and_project.schedule(schedule, marg_cpt_set, hard_variables);
805
806 // there should be only one tensor in new_cpt_list
807 if (new_cpt_list.size() != 1) {
809 "the projection of a tensor containing " << "hard evidence is empty!");
810 }
811 auto projected_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
812 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
813 *new_cpt_list.begin()));
814 res.insert(projected_pot);
815 }
816 }
817
818 // if the node received some soft evidence, add it
819 if (evidence.exists(node) && !hard_evidence.exists(node)) {
820 const IScheduleMultiDim* pot
821 = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[node], false);
822 res.insert(pot);
823 }
824 }
825
826 return res;
827 }
828
829 // get the CPT + evidence of a node projected w.r.t. hard evidence
830 template < GUM_Numeric GUM_SCALAR >
831 std::pair< Set< const Tensor< GUM_SCALAR >* >, Set< const Tensor< GUM_SCALAR >* > >
832 VariableElimination< GUM_SCALAR >::_NodeTensors_(NodeId node) {
833 std::pair< _TensorSet_, _TensorSet_ > res;
834 const auto& bn = this->BN();
835
836 // get the CPT's of the node
837 // beware: all the tensors that are defined over some nodes
838 // including hard evidence must be projected so that these nodes are
839 // removed from the tensor
840 // also beware that the CPT of a hard evidence node may be defined over
841 // parents that do not belong to _graph_ and that are not hard evidence.
842 // In this case, those parents have been removed by d-separation and it is
843 // easy to show that, in this case all the parents have been removed, so
844 // that the CPT does not need to be taken into account
845 const auto& evidence = this->evidence();
846 const auto& hard_evidence = this->hardEvidence();
847 const auto& hard_ev_nodes = this->hardEvidenceNodes();
848 if (_graph_.exists(node) || hard_ev_nodes.contains(node)) {
849 const Tensor< GUM_SCALAR >& cpt = bn.cpt(node);
850 const auto& variables = cpt.variablesSequence();
851
852 // check if the parents of a hard evidence node do not belong to _graph_
853 // and are not themselves hard evidence. In this case, discard the CPT as
854 // it is useless for inference (see the above comment)
855 if (hard_ev_nodes.contains(node)) {
856 for (const auto var: variables) {
857 NodeId xnode = bn.nodeId(*var);
858 if (!hard_ev_nodes.contains(xnode) && !_graph_.existsNode(xnode)) return res;
859 }
860 }
861
862 // get the list of nodes with hard evidence in cpt
863 NodeSet hard_nodes(variables.size());
864 for (const auto var: variables) {
865 NodeId xnode = bn.nodeId(*var);
866 if (hard_ev_nodes.contains(xnode)) hard_nodes.insert(xnode);
867 }
868
869 // if hard_nodes contains hard evidence nodes, perform a projection
870 // and insert the result into the appropriate clique, else insert
871 // directly cpt into the clique
872 if (hard_nodes.empty()) {
873 res.first.insert(&cpt);
874 } else {
875 // marginalize out the hard evidence nodes: if the cpt is defined
876 // only over nodes that received hard evidence, do not consider it
877 // as a tensor anymore
878 if (hard_nodes.size() != variables.size()) {
879 // perform the projection with a combine and project instance
880 gum::VariableSet hard_variables;
881 _TensorSet_ marg_cpt_set(1 + hard_nodes.size());
882 marg_cpt_set.insert(&cpt);
883
884 for (const auto xnode: hard_nodes) {
885 marg_cpt_set.insert(evidence[xnode]);
886 hard_variables.insert(&(bn.variable(xnode)));
887 }
888 // perform the combination of those tensors and their projection
889 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(
890 _combination_op_,
891 VENewprojTensor);
892 _TensorSet_ new_cpt_list = combine_and_project.execute(marg_cpt_set, hard_variables);
893
894 // there should be only one tensor in new_cpt_list
895 if (new_cpt_list.size() != 1) {
896 // remove the CPT created to avoid memory leaks
897 for (auto pot: new_cpt_list) {
898 if (!marg_cpt_set.contains(pot)) delete pot;
899 }
901 "the projection of a tensor containing " << "hard evidence is empty!");
902 }
903 const Tensor< GUM_SCALAR >* projected_cpt = *(new_cpt_list.begin());
904 res.first.insert(projected_cpt);
905 res.second.insert(projected_cpt);
906 }
907 }
908
909 // if the node received some soft evidence, add it
910 if (evidence.exists(node) && !hard_evidence.exists(node)) {
911 res.first.insert(this->evidence()[node]);
912 }
913 }
914
915 return res;
916 }
917
918 // creates the message sent by clique from_id to clique to_id
919 template < GUM_Numeric GUM_SCALAR >
920 std::pair< Set< const Tensor< GUM_SCALAR >* >, Set< const Tensor< GUM_SCALAR >* > >
921 VariableElimination< GUM_SCALAR >::_produceMessage_(
922 NodeId from_id,
923 NodeId to_id,
924 std::pair< Set< const Tensor< GUM_SCALAR >* >, Set< const Tensor< GUM_SCALAR >* > >&&
925 incoming_messages) {
926 // get the messages sent by adjacent nodes to from_id
927 std::pair< _TensorSet_, _TensorSet_ > pot_list(std::move(incoming_messages));
928
929 // get the tensors of the clique
930 for (const auto node: _clique_to_nodes_[from_id]) {
931 auto new_pots = _NodeTensors_(node);
932 pot_list.first += new_pots.first;
933 pot_list.second += new_pots.second;
934 }
935
936 // if from_id = to_id: this is the endpoint of a collect
937 if (!_JT_->existsEdge(from_id, to_id)) {
938 return pot_list;
939 } else {
940 // get the set of variables that need to be removed from the tensors
941 const NodeSet& from_clique = _JT_->clique(from_id);
942 const NodeSet& separator = _JT_->separator(from_id, to_id);
943 gum::VariableSet del_vars(from_clique.size());
944 gum::VariableSet kept_vars(separator.size());
945 const auto& bn = this->BN();
946
947 for (const auto node: from_clique) {
948 if (!separator.contains(node)) {
949 del_vars.insert(&(bn.variable(node)));
950 } else {
951 kept_vars.insert(&(bn.variable(node)));
952 }
953 }
954
955 // pot_list now contains all the tensors to multiply and marginalize
956 // => combine the messages
957 _TensorSet_ new_pot_list = _marginalizeOut_(pot_list.first, del_vars, kept_vars);
958
959 // remove the unnecessary temporary messages
960 for (auto iter = pot_list.second.beginSafe(); iter != pot_list.second.endSafe(); ++iter) {
961 if (!new_pot_list.contains(*iter)) {
962 delete *iter;
963 pot_list.second.erase(iter);
964 }
965 }
966
967 // keep track of all the newly created tensors
968 for (const auto pot: new_pot_list) {
969 if (!pot_list.first.contains(pot)) { pot_list.second.insert(pot); }
970 }
971
972 // return the new set of tensors
973 return std::pair< _TensorSet_, _TensorSet_ >(std::move(new_pot_list),
974 std::move(pot_list.second));
975 }
976 }
977
978 // creates the message sent by clique from_id to clique to_id
979 template < GUM_Numeric GUM_SCALAR >
980 Set< const IScheduleMultiDim* > VariableElimination< GUM_SCALAR >::_produceMessage_(
981 Schedule& schedule,
982 NodeId from_id,
983 NodeId to_id,
984 Set< const IScheduleMultiDim* >&& incoming_messages) {
985 // get the messages sent by adjacent nodes to from_id
986 _ScheduleMultiDimSet_ pot_list(std::move(incoming_messages));
987
988 // get the tensors of the clique
989 for (const auto node: _clique_to_nodes_[from_id]) {
990 pot_list += _NodeTensors_(schedule, node);
991 }
992
993 // if from_id = to_id: this is the endpoint of a collect
994 if (!_JT_->existsEdge(from_id, to_id)) {
995 return pot_list;
996 } else {
997 // get the set of variables that need be removed from the tensors
998 const NodeSet& from_clique = _JT_->clique(from_id);
999 const NodeSet& separator = _JT_->separator(from_id, to_id);
1000 gum::VariableSet del_vars(from_clique.size());
1001 gum::VariableSet kept_vars(separator.size());
1002 const auto& bn = this->BN();
1003
1004 for (const auto node: from_clique) {
1005 if (!separator.contains(node)) {
1006 del_vars.insert(&(bn.variable(node)));
1007 } else {
1008 kept_vars.insert(&(bn.variable(node)));
1009 }
1010 }
1011
1012 // pot_list now contains all the tensors to multiply and marginalize
1013 // => combine the messages
1014 _ScheduleMultiDimSet_ new_pot_list
1015 = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
1016
1017 // remove the unnecessary temporary messages
1018 for (auto pot: pot_list) {
1019 if (!new_pot_list.contains(pot)) {
1020 const auto sched_pot
1021 = static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot);
1022 schedule.emplaceDeletion(*sched_pot);
1023 }
1024 }
1025
1026 // return the new set of tensors
1027 return new_pot_list;
1028 }
1029 }
1030
1031 // remove variables del_vars from the list of tensors pot_list
1032 template < GUM_Numeric GUM_SCALAR >
1033 Set< const Tensor< GUM_SCALAR >* > VariableElimination< GUM_SCALAR >::_marginalizeOut_(
1034 Set< const Tensor< GUM_SCALAR >* > pot_list,
1035 gum::VariableSet& del_vars,
1036 gum::VariableSet& kept_vars) {
1037 // if pot list is empty, do nothing. This may happen when there are many barren variables
1038 if (pot_list.empty()) { return _TensorSet_(); }
1039
1040 // use d-separation analysis to check which tensors shall be combined
1041 // _findRelevantTensorsXX_(pot_list, kept_vars);
1042
1043 // remove the tensors corresponding to barren variables if we want
1044 // to exploit barren nodes
1045 _TensorSet_ barren_projected_tensors;
1046 if (_barren_nodes_type_ == FindBarrenNodesType::FIND_BARREN_NODES) {
1047 barren_projected_tensors = _removeBarrenVariables_(pot_list, del_vars);
1048 }
1049
1050 // Combine and project the remaining tensors
1051 _TensorSet_ new_pot_list;
1052 if (pot_list.size() == 1) {
1053 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
1054 auto pot = projector.execute(**(pot_list.begin()), del_vars);
1055 new_pot_list.insert(pot);
1056 } else if (pot_list.size() > 1) {
1057 // create a combine and project operator that will perform the
1058 // marginalization
1059 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(_combination_op_,
1060 _projection_op_);
1061 new_pot_list = combine_and_project.execute(pot_list, del_vars);
1062 }
1063
1064 // remove all the tensors that were created due to projections of
1065 // barren nodes and that are not part of the new_pot_list: these
1066 // tensors were just temporary tensors
1067 for (auto iter = barren_projected_tensors.beginSafe();
1068 iter != barren_projected_tensors.endSafe();
1069 ++iter) {
1070 if (!new_pot_list.exists(*iter)) delete *iter;
1071 }
1072
1073 return new_pot_list;
1074 }
1075
1076 // remove variables del_vars from the list of tensors pot_list
1077 template < GUM_Numeric GUM_SCALAR >
1078 Set< const IScheduleMultiDim* >
1079 VariableElimination< GUM_SCALAR >::_marginalizeOut_(Schedule& schedule,
1080 Set< const IScheduleMultiDim* > pot_list,
1081 gum::VariableSet& del_vars,
1082 gum::VariableSet& kept_vars) {
1083 // if pot list is empty, do nothing. This may happen when there are only barren variables
1084 if (pot_list.empty()) { return _ScheduleMultiDimSet_(); }
1085
1086 // use d-separation analysis to check which tensors shall be combined
1087 // _findRelevantTensorsXX_(pot_list, kept_vars);
1088
1089 // now, let's guarantee that all the tensors to be combined and projected
1090 // belong to the schedule
1091 for (const auto pot: pot_list) {
1092 if (!schedule.existsScheduleMultiDim(pot->id())) schedule.emplaceScheduleMultiDim(*pot);
1093 }
1094
1095 // remove the tensors corresponding to barren variables if we want
1096 // to exploit barren nodes
1097 _ScheduleMultiDimSet_ barren_projected_tensors;
1098 if (_barren_nodes_type_ == FindBarrenNodesType::FIND_BARREN_NODES) {
1099 barren_projected_tensors = _removeBarrenVariables_(schedule, pot_list, del_vars);
1100 }
1101
1102 // Combine and project the tensors
1103 _ScheduleMultiDimSet_ new_pot_list;
1104 if (pot_list.size() == 1) { // only one tensor, so just project it
1105 MultiDimProjection< Tensor< GUM_SCALAR > > projector(_projection_op_);
1106 auto xpot = projector.schedule(schedule, *(pot_list.begin()), del_vars);
1107 new_pot_list.insert(xpot);
1108 } else if (pot_list.size() > 1) {
1109 // create a combine and project operator that will perform the
1110 // marginalization
1111 MultiDimCombineAndProjectDefault< Tensor< GUM_SCALAR > > combine_and_project(_combination_op_,
1112 _projection_op_);
1113 new_pot_list = combine_and_project.schedule(schedule, pot_list, del_vars);
1114 }
1115
1116 // remove all the tensors that were created due to projections of
1117 // barren nodes and that are not part of the new_pot_list: these
1118 // tensors were just temporary tensors
1119 for (auto pot: barren_projected_tensors) {
1120 if (!new_pot_list.exists(pot)) {
1121 const auto sched_pot = static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot);
1122 schedule.emplaceDeletion(*sched_pot);
1123 }
1124 }
1125
1126 return new_pot_list;
1127 }
1128
1129 // performs a whole inference
1130 template < GUM_Numeric GUM_SCALAR >
1131 void VariableElimination< GUM_SCALAR >::makeInference_() {}
1132
1134 template < GUM_Numeric GUM_SCALAR >
1135 Tensor< GUM_SCALAR >* VariableElimination< GUM_SCALAR >::unnormalizedJointPosterior_(NodeId id) {
1136 // hard evidence do not belong to the join tree
1137 // # TODO: check for sets of inconsistent hard evidence
1138 if (this->hardEvidenceNodes().contains(id)) {
1139 return new Tensor< GUM_SCALAR >(*(this->evidence()[id]));
1140 }
1141
1142 // if we still need to perform some inference task, do it
1143 _createNewJT_(NodeSet{id});
1144
1145 // here, we determine whether we should use schedules during the inference.
1146 // the rule is: if the sum of the domain sizes of the cliques is greater
1147 // than a threshold, use schedules
1148 double overall_size = 0;
1149 for (const auto clique: *_JT_) {
1150 double clique_size = 1.0;
1151 for (const auto node: _JT_->clique(clique))
1152 clique_size *= this->domainSizes()[node];
1153 overall_size += clique_size;
1154 }
1155 const bool use_schedules = (overall_size > _schedule_threshold_);
1156
1157 if (use_schedules) {
1158 Schedule schedule;
1159 return _unnormalizedJointPosterior_(schedule, id);
1160 } else {
1161 return _unnormalizedJointPosterior_(id);
1162 }
1163 }
1164
1166 template < GUM_Numeric GUM_SCALAR >
1167 Tensor< GUM_SCALAR >* VariableElimination< GUM_SCALAR >::_unnormalizedJointPosterior_(NodeId id) {
1168 const auto& bn = this->BN();
1169
1170 NodeId clique_of_id = _node_to_clique_[id];
1171 std::pair< _TensorSet_, _TensorSet_ > pot_list = _collectMessage_(clique_of_id, clique_of_id);
1172
1173 // get the set of variables that need be removed from the tensors
1174 const NodeSet& nodes = _JT_->clique(clique_of_id);
1175 gum::VariableSet kept_vars{&(bn.variable(id))};
1176 gum::VariableSet del_vars(nodes.size());
1177 for (const auto node: nodes) {
1178 if (node != id) del_vars.insert(&(bn.variable(node)));
1179 }
1180
1181 // pot_list now contains all the tensors to multiply and marginalize
1182 // => combine the messages
1183 _TensorSet_ new_pot_list = _marginalizeOut_(pot_list.first, del_vars, kept_vars);
1184 Tensor< GUM_SCALAR >* joint = nullptr;
1185
1186 if (new_pot_list.size() == 0) {
1187 joint = new Tensor< GUM_SCALAR >;
1188 for (const auto var: kept_vars)
1189 *joint << *var;
1190 } else {
1191 if (new_pot_list.size() == 1) {
1192 joint = const_cast< Tensor< GUM_SCALAR >* >(*(new_pot_list.begin()));
1193 // if joint already existed, create a copy, so that we can put it into
1194 // the _target_posterior_ property
1195 if (pot_list.first.exists(joint)) {
1196 joint = new Tensor< GUM_SCALAR >(*joint);
1197 } else {
1198 // remove the joint from new_pot_list so that it will not be
1199 // removed just after the else block
1200 new_pot_list.clear();
1201 }
1202 } else {
1203 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1204 joint = fast_combination.execute(new_pot_list);
1205 }
1206 }
1207
1208 // remove the tensors that were created in new_pot_list
1209 for (auto pot: new_pot_list)
1210 if (!pot_list.first.exists(pot)) delete pot;
1211
1212 // remove all the temporary tensors created in pot_list
1213 for (auto pot: pot_list.second)
1214 delete pot;
1215
1216 // check that the joint posterior is different from a 0 vector: this would
1217 // indicate that some hard evidence are not compatible (their joint
1218 // probability is equal to 0)
1219 bool nonzero_found = false;
1220 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1221 if ((*joint)[inst]) {
1222 nonzero_found = true;
1223 break;
1224 }
1225 }
1226 if (!nonzero_found) {
1227 // remove joint from memory to avoid memory leaks
1228 delete joint;
1230 "some evidence entered into the Bayes "
1231 "net are incompatible (their joint proba = 0)");
1232 }
1233
1234 return joint;
1235 }
1236
1238 template < GUM_Numeric GUM_SCALAR >
1239 Tensor< GUM_SCALAR >*
1240 VariableElimination< GUM_SCALAR >::_unnormalizedJointPosterior_(Schedule& schedule,
1241 NodeId id) {
1242 const auto& bn = this->BN();
1243
1244 NodeId clique_of_id = _node_to_clique_[id];
1245 _ScheduleMultiDimSet_ pot_list = _collectMessage_(schedule, clique_of_id, clique_of_id);
1246
1247 // get the set of variables that need be removed from the tensors
1248 const NodeSet& nodes = _JT_->clique(clique_of_id);
1249 gum::VariableSet kept_vars{&(bn.variable(id))};
1250 gum::VariableSet del_vars(nodes.size());
1251 for (const auto node: nodes) {
1252 if (node != id) del_vars.insert(&(bn.variable(node)));
1253 }
1254
1255 // pot_list now contains all the tensors to multiply and marginalize
1256 // => combine the messages
1257 _ScheduleMultiDimSet_ new_pot_list = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
1258 Tensor< GUM_SCALAR >* joint = nullptr;
1259 ScheduleMultiDim< Tensor< GUM_SCALAR > >* resulting_pot = nullptr;
1260
1261 if (new_pot_list.size() == 0) {
1262 joint = new Tensor< GUM_SCALAR >;
1263 for (const auto var: kept_vars)
1264 *joint << *var;
1265 } else {
1266 auto& scheduler = this->scheduler();
1267 if (new_pot_list.size() == 1) {
1268 scheduler.execute(schedule);
1269 resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1270 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(*new_pot_list.begin()));
1271 } else {
1272 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1273 const IScheduleMultiDim* pot = fast_combination.schedule(schedule, new_pot_list);
1274 scheduler.execute(schedule);
1275 resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1276 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot));
1277 }
1278
1279 // if resulting_pot already existed, create a copy, so that we can put it into
1280 // the _target_posteriors_ property
1281 if (pot_list.exists(resulting_pot)) {
1282 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
1283 } else {
1284 joint = resulting_pot->exportMultiDim();
1285 }
1286 }
1287
1288 // check that the joint posterior is different from a 0 vector: this would
1289 // indicate that some hard evidence are not compatible (their joint
1290 // probability is equal to 0)
1291 bool nonzero_found = false;
1292 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1293 if ((*joint)[inst]) {
1294 nonzero_found = true;
1295 break;
1296 }
1297 }
1298 if (!nonzero_found) {
1299 // remove joint from memory to avoid memory leaks
1300 delete joint;
1302 "some evidence entered into the Bayes "
1303 "net are incompatible (their joint proba = 0)");
1304 }
1305
1306 return joint;
1307 }
1308
1310 template < GUM_Numeric GUM_SCALAR >
1311 const Tensor< GUM_SCALAR >& VariableElimination< GUM_SCALAR >::posterior_(NodeId id) {
1312 // compute the joint posterior and normalize
1313 auto joint = unnormalizedJointPosterior_(id);
1314 if (joint->sum() != 1) // hard test for ReadOnly CPT (as aggregator)
1315 joint->normalize();
1316
1317 if (_target_posterior_ != nullptr) delete _target_posterior_;
1318 _target_posterior_ = joint;
1319
1320 return *joint;
1321 }
1322
1323 // returns the marginal a posteriori proba of a given node
1324 template < GUM_Numeric GUM_SCALAR >
1325 Tensor< GUM_SCALAR >*
1326 VariableElimination< GUM_SCALAR >::unnormalizedJointPosterior_(const NodeSet& set) {
1327 // hard evidence do not belong to the join tree, so extract the nodes
1328 // from targets that are not hard evidence
1329 NodeSet targets = set, hard_ev_nodes(this->hardEvidenceNodes().size());
1330 for (const auto node: this->hardEvidenceNodes()) {
1331 if (targets.contains(node)) {
1332 targets.erase(node);
1333 hard_ev_nodes.insert(node);
1334 }
1335 }
1336
1337 // if all the nodes have received hard evidence, then compute the
1338 // joint posterior directly by multiplying the hard evidence tensors
1339 const auto& evidence = this->evidence();
1340 if (targets.empty()) {
1341 _TensorSet_ pot_list;
1342 for (const auto node: set) {
1343 pot_list.insert(evidence[node]);
1344 }
1345 if (pot_list.size() == 1) {
1346 return new Tensor< GUM_SCALAR >(**(pot_list.begin()));
1347 } else {
1348 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1349 return fast_combination.execute(pot_list);
1350 }
1351 }
1352
1353 // if we still need to perform some inference task, do it
1354 _createNewJT_(set);
1355
1356 // here, we determine whether we should use schedules during the inference.
1357 // the rule is: if the sum of the domain sizes of the cliques is greater
1358 // than a threshold, use schedules
1359 double overall_size = 0;
1360 for (const auto clique: *_JT_) {
1361 double clique_size = 1.0;
1362 for (const auto node: _JT_->clique(clique))
1363 clique_size *= this->domainSizes()[node];
1364 overall_size += clique_size;
1365 }
1366 const bool use_schedules = (overall_size > _schedule_threshold_);
1367
1368 if (use_schedules) {
1369 Schedule schedule;
1370 return _unnormalizedJointPosterior_(schedule, set, targets, hard_ev_nodes);
1371 } else {
1372 return _unnormalizedJointPosterior_(set, targets, hard_ev_nodes);
1373 }
1374 }
1375
1376 // returns the marginal a posteriori proba of a given node
1377 template < GUM_Numeric GUM_SCALAR >
1378 Tensor< GUM_SCALAR >* VariableElimination< GUM_SCALAR >::_unnormalizedJointPosterior_(
1379 const NodeSet& set,
1380 const NodeSet& targets,
1381 const NodeSet& hard_ev_nodes) {
1382 std::pair< _TensorSet_, _TensorSet_ > pot_list
1383 = _collectMessage_(_targets2clique_, _targets2clique_);
1384
1385 // get the set of variables that need be removed from the tensors
1386 const NodeSet& nodes = _JT_->clique(_targets2clique_);
1387 gum::VariableSet del_vars(nodes.size());
1388 gum::VariableSet kept_vars(targets.size());
1389 const auto& bn = this->BN();
1390 for (const auto node: nodes) {
1391 if (!targets.contains(node)) {
1392 del_vars.insert(&(bn.variable(node)));
1393 } else {
1394 kept_vars.insert(&(bn.variable(node)));
1395 }
1396 }
1397
1398 // pot_list now contains all the tensors to multiply and marginalize
1399 // => combine the messages
1400 _TensorSet_ new_pot_list = _marginalizeOut_(pot_list.first, del_vars, kept_vars);
1401 Tensor< GUM_SCALAR >* joint = nullptr;
1402
1403 if ((new_pot_list.size() == 1) && hard_ev_nodes.empty()) {
1404 joint = const_cast< Tensor< GUM_SCALAR >* >(*(new_pot_list.begin()));
1405 // if pot already existed, create a copy, so that we can put it into
1406 // the _target_posteriors_ property
1407 if (pot_list.first.exists(joint)) {
1408 joint = new Tensor< GUM_SCALAR >(*joint);
1409 } else {
1410 // remove the joint from new_pot_list so that it will not be
1411 // removed just after the next else block
1412 new_pot_list.clear();
1413 }
1414 } else {
1415 // combine all the tensors in new_pot_list with all the hard evidence
1416 // of the nodes in set
1417 const auto& evidence = this->evidence();
1418 _TensorSet_ new_new_pot_list = new_pot_list;
1419 for (const auto node: hard_ev_nodes) {
1420 new_new_pot_list.insert(evidence[node]);
1421 }
1422 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1423 joint = fast_combination.execute(new_new_pot_list);
1424 }
1425
1426 // remove the tensors that were created in new_pot_list
1427 for (auto pot: new_pot_list)
1428 if (!pot_list.first.exists(pot)) delete pot;
1429
1430 // remove all the temporary tensors created in pot_list
1431 for (auto pot: pot_list.second)
1432 delete pot;
1433
1434 // check that the joint posterior is different from a 0 vector: this would
1435 // indicate that some hard evidence are not compatible
1436 bool nonzero_found = false;
1437 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1438 if ((*joint)[inst]) {
1439 nonzero_found = true;
1440 break;
1441 }
1442 }
1443 if (!nonzero_found) {
1444 // remove joint from memory to avoid memory leaks
1445 delete joint;
1447 "some evidence entered into the Bayes "
1448 "net are incompatible (their joint proba = 0)");
1449 }
1450
1451 return joint;
1452 }
1453
1454 // returns the marginal a posteriori proba of a given node
1455 template < GUM_Numeric GUM_SCALAR >
1456 Tensor< GUM_SCALAR >* VariableElimination< GUM_SCALAR >::_unnormalizedJointPosterior_(
1457 Schedule& schedule,
1458 const NodeSet& set,
1459 const NodeSet& targets,
1460 const NodeSet& hard_ev_nodes) {
1461 _ScheduleMultiDimSet_ pot_list = _collectMessage_(schedule, _targets2clique_, _targets2clique_);
1462
1463 // get the set of variables that need be removed from the tensors
1464 const NodeSet& nodes = _JT_->clique(_targets2clique_);
1465 gum::VariableSet del_vars(nodes.size());
1466 gum::VariableSet kept_vars(targets.size());
1467 const auto& bn = this->BN();
1468 for (const auto node: nodes) {
1469 if (!targets.contains(node)) {
1470 del_vars.insert(&(bn.variable(node)));
1471 } else {
1472 kept_vars.insert(&(bn.variable(node)));
1473 }
1474 }
1475
1476 // pot_list now contains all the tensors to multiply and marginalize
1477 // => combine the messages
1478 _ScheduleMultiDimSet_ new_pot_list = _marginalizeOut_(schedule, pot_list, del_vars, kept_vars);
1479 ScheduleMultiDim< Tensor< GUM_SCALAR > >* resulting_pot = nullptr;
1480 auto& scheduler = this->scheduler();
1481
1482 if ((new_pot_list.size() == 1) && hard_ev_nodes.empty()) {
1483 scheduler.execute(schedule);
1484 resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1485 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(*new_pot_list.begin()));
1486 } else {
1487 // combine all the tensors in new_pot_list with all the hard evidence
1488 // of the nodes in set
1489 const auto& evidence = this->evidence();
1490 for (const auto node: hard_ev_nodes) {
1491 auto new_pot_ev = schedule.insertTable< Tensor< GUM_SCALAR > >(*evidence[node], false);
1492 new_pot_list.insert(new_pot_ev);
1493 }
1494 MultiDimCombinationDefault< Tensor< GUM_SCALAR > > fast_combination(_combination_op_);
1495 const auto pot = fast_combination.schedule(schedule, new_pot_list);
1496 scheduler.execute(schedule);
1497 resulting_pot = const_cast< ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(
1498 static_cast< const ScheduleMultiDim< Tensor< GUM_SCALAR > >* >(pot));
1499 }
1500
1501 // if pot already existed, create a copy, so that we can put it into
1502 // the _target_posteriors_ property
1503 Tensor< GUM_SCALAR >* joint = nullptr;
1504 if (pot_list.exists(resulting_pot)) {
1505 joint = new Tensor< GUM_SCALAR >(resulting_pot->multiDim());
1506 } else {
1507 joint = resulting_pot->exportMultiDim();
1508 }
1509
1510 // check that the joint posterior is different from a 0 vector: this would
1511 // indicate that some hard evidence are not compatible
1512 bool nonzero_found = false;
1513 for (Instantiation inst(*joint); !inst.end(); ++inst) {
1514 if ((*joint)[inst]) {
1515 nonzero_found = true;
1516 break;
1517 }
1518 }
1519 if (!nonzero_found) {
1520 // remove joint from memory to avoid memory leaks
1521 delete joint;
1523 "some evidence entered into the Bayes "
1524 "net are incompatible (their joint proba = 0)");
1525 }
1526
1527 return joint;
1528 }
1529
1531 template < GUM_Numeric GUM_SCALAR >
1532 const Tensor< GUM_SCALAR >&
1533 VariableElimination< GUM_SCALAR >::jointPosterior_(const NodeSet& set) {
1534 // compute the joint posterior and normalize
1535 auto joint = unnormalizedJointPosterior_(set);
1536 joint->normalize();
1537
1538 if (_target_posterior_ != nullptr) delete _target_posterior_;
1539 _target_posterior_ = joint;
1540
1541 return *joint;
1542 }
1543
1545 template < GUM_Numeric GUM_SCALAR >
1546 const Tensor< GUM_SCALAR >&
1547 VariableElimination< GUM_SCALAR >::jointPosterior_(const NodeSet& wanted_target,
1548 const NodeSet& declared_target) {
1549 return jointPosterior_(wanted_target);
1550 }
1551
1552 template < GUM_Numeric GUM_SCALAR >
1553 Tensor< GUM_SCALAR > VENewmultiTensor(const Tensor< GUM_SCALAR >& t1,
1554 const Tensor< GUM_SCALAR >& t2) {
1555 return t1 * t2;
1556 }
1557
1558 template < GUM_Numeric GUM_SCALAR >
1559 Tensor< GUM_SCALAR > VENewprojTensor(const Tensor< GUM_SCALAR >& t1,
1560 const gum::VariableSet& del_vars) {
1561 return t1.sumOut(del_vars);
1562 }
1563
1564 template < GUM_Numeric GUM_SCALAR >
1565 void VariableElimination< GUM_SCALAR >::onStateChanged_() {}
1566
1567} /* namespace gum */
1568
1569#endif // DOXYGEN_SHOULD_SKIP_THIS
The BayesBall algorithm (as described by Schachter).
Detect barren nodes for inference in Bayesian networks.
An algorithm for converting a join tree into a binary join tree.
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
VariableElimination(const IBayesNet< GUM_SCALAR > *BN, RelevantTensorsFinderType=RelevantTensorsFinderType::DSEP_BAYESBALL_TENSORS, FindBarrenNodesType=FindBarrenNodesType::FIND_BARREN_NODES)
default constructor
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 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...
Implementation of a variable elimination algorithm for inference in Bayesian networks.