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