aGrUM 2.3.2
a C++ library for (probabilistic) graphical models
cliqueGraph.cpp
Go to the documentation of this file.
1/****************************************************************************
2 * This file is part of the aGrUM/pyAgrum library. *
3 * *
4 * Copyright (c) 2005-2025 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-2025 *
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
47#include <algorithm>
48#include <sstream>
49
51
52
53#ifdef GUM_NO_INLINE
55#endif // GU%_NO_INLINE
56
57#ifndef DOXYGEN_SHOULD_SKIP_THIS
58
59namespace gum {
60
61 /* =================================================================== */
62 /* =================================================================== */
63 /* === IMPLEMENTATION OF GUM_CLIQUE_GRAPH === */
64 /* =================================================================== */
65 /* =================================================================== */
66
68
70 bool nodes_resize_policy,
71 Size edges_size,
72 bool edges_resize_policy) :
73 NodeGraphPart(nodes_size, nodes_resize_policy),
74 UndiGraph(nodes_size, nodes_resize_policy, edges_size, edges_resize_policy) {
75 // for debugging purposes
76 GUM_CONSTRUCTOR(CliqueGraph)
77 }
78
80
81 CliqueGraph::CliqueGraph(const CliqueGraph& from) :
82 NodeGraphPart(from), // needed because NodeGraphPart is a virtual inherited
83 UndiGraph(from), // class (see C++ FAQ Lite #25.12 for details)
84 _cliques_(from._cliques_), _separators_(from._separators_) { // for debugging purposes
85 GUM_CONS_CPY(CliqueGraph)
86 }
87
89
90 CliqueGraph::~CliqueGraph() { // for debugging purposes
91 GUM_DESTRUCTOR(CliqueGraph)
92 }
93
96
97 std::vector< NodeId > CliqueGraph::containerPath(const NodeId node1, const NodeId node2) const {
98 // get a path from a _clique_ containing node1 to a _clique_ containing
99 // node2
100 std::vector< NodeId > path = undirectedPath(container(node1), container(node2));
101
102 // it may happen that the path contains several nodes containing node1 and
103 // node2. Hence we shall remove the superfluous nodes
104 while ((path.size() >= 2) && (clique(path[path.size() - 2]).contains(node2)))
105 path.pop_back();
106
107 while ((path.size() >= 2) && (clique(path[1]).contains(node1)))
108 path.erase(path.begin());
109
110 return path;
111 }
112
116
117 void CliqueGraph::addToClique(const NodeId clique_id, const NodeId node_id) {
118 // get the current clique set
119 NodeSet& clique = _cliques_[clique_id];
120
121 // check if the node already exists, in which case throw an exception
122 if (clique.contains(node_id)) {
123 GUM_ERROR(DuplicateElement, "the clique set already contains the node " << node_id)
124 }
125
126 clique.insert(node_id);
127
128 // update the _separators_ adjacent to clique 'id'
129 for (const auto nei: neighbours(clique_id))
130 if (_cliques_[nei].contains(node_id)) _separators_[Edge(nei, clique_id)].insert(node_id);
131 }
132
134
135 void CliqueGraph::eraseFromClique(const NodeId clique_id, const NodeId node_id) {
136 // get the current _clique_ set
137 NodeSet& clique = _cliques_[clique_id];
138
139 // check if the node does not exist, in which case throw an exception
140 if (clique.contains(node_id)) {
141 clique.erase(node_id);
142
143 // update the _separators_ adjacent to _clique_ 'id'
144 for (const auto nei: neighbours(clique_id)) {
145 Edge edge(nei, clique_id);
146
147 if (_separators_[edge].contains(node_id)) _separators_[edge].erase(node_id);
148 }
149 }
150 }
151
153
154 bool CliqueGraph::_runningIntersectionDFS_(const NodeId clique,
155 const NodeId from,
156 CliqueGraph::_RunningIntersect_& infos_DFS) const {
157 // check that no node in the clique belongs to the set of nodes belonging to
158 // other connected components of the cliqueGraph
159 const NodeSet& nodes_clique = _cliques_[clique];
160
161 for (const auto node: nodes_clique)
162 if (infos_DFS.nodes_other_components.contains(node)) return false;
163
164 // update the structure that keeps track of the cliques that still require
165 // chains to access some of their nodes
166 for (const auto node: nodes_clique)
167 if (!infos_DFS.nodes_DFS_forbidden.contains(node))
168 infos_DFS.cliques_DFS_chain[clique].erase(node);
169
170 // if the clique had already been visited, no need to see its neighbours
171 // or to update the list of visited nodes
172 if (infos_DFS.visited_cliques.contains(clique)) return true;
173
174 // update the list of nodes visited during the DFS
175 for (const auto node: nodes_clique)
176 if (!infos_DFS.nodes_DFS_seen.contains(node)) infos_DFS.nodes_DFS_seen.insert(node);
177
178 // update the fact that the clique has been visited
179 infos_DFS.visited_cliques.insert(clique);
180
181 // check the neighbours that are different from "from" and that have not
182 // been visited yet
183
184 for (const auto otherID: neighbours(clique))
185 if (otherID != from) {
186 // update the list of forbidden nodes in the DFS, i.e., the nodes that
187 // belong to the clique but not to the separator
188 const Edge edge(otherID, clique);
189 const NodeSet& from_separ = _separators_[edge];
190
191 for (const auto node: nodes_clique) {
192 if (!from_separ.contains(node)) infos_DFS.nodes_DFS_forbidden.insert(node);
193 }
194
195 // check the neighbour
196 if (!_runningIntersectionDFS_(otherID, clique, infos_DFS)) return false;
197
198 // remove from the forbidden list the nodes that belong to clique
199 for (const auto node: nodes_clique)
200 infos_DFS.nodes_DFS_forbidden.erase(node);
201
202 // check again the structure that keeps track of the cliques that still
203 // require chains to access some of their nodes: the chain may be
204 // the neighbour we just encountered
205 for (const auto node: nodes_clique) {
206 if (!infos_DFS.nodes_DFS_forbidden.contains(node))
207 infos_DFS.cliques_DFS_chain[clique].erase(node);
208 }
209 }
210
211 // when a node is terminal, i.e., it has at most one neighbour, add its
212 // nodes
213 // to the nodes forbidden by the DFS. It will prevent non adjacent extremal
214 // cliques to contain the same node while this one does not belong to any
215 // separator
216 if (neighbours(clique).size() <= 1)
217 for (const auto node: nodes_clique)
218 if (!infos_DFS.nodes_DFS_forbidden.contains(node))
219 infos_DFS.nodes_DFS_forbidden.insert(node);
220
221 // here everything was OK. A priori, the running intersection property holds
222 return true;
223 }
224
226
227 bool CliqueGraph::hasRunningIntersection() const {
228 // create a RunningIntersect structure and initialize it
229 _RunningIntersect_ infos_DFS;
230 infos_DFS.cliques_DFS_chain = _cliques_;
231
232 // while there exist unvisited cliques, perform a DFS on them
233 for (const auto DFSnode: nodes())
234 if (!infos_DFS.visited_cliques.contains(DFSnode)) {
235 // no nodes are forbidden priori in the DFS
236 infos_DFS.nodes_DFS_forbidden.clear();
237
238 // no node has already been seen in the DFS
239 infos_DFS.nodes_DFS_seen.clear();
240
241 // here iter_DFS points on a clique that has not been visited yet
242 // visit the clique graph from this clique
243 if (!_runningIntersectionDFS_(DFSnode, DFSnode, infos_DFS)) return false;
244
245 // the nodes that were seen during the DFS belong to a connected
246 // component
247 // that is different from the connected components of the subsequent DFS
248 for (const auto node: infos_DFS.nodes_DFS_seen)
249 if (!infos_DFS.nodes_other_components.contains(node))
250 infos_DFS.nodes_other_components.insert(node);
251 }
252
253 // check that no clique requires an additional chain to guarantee the
254 // running intersection property
255 for (const auto& [node, nodes]: infos_DFS.cliques_DFS_chain)
256 if (!nodes.empty()) return false;
257
258 return true;
259 }
260
262
263 bool CliqueGraph::operator==(const CliqueGraph& from) const {
264 // check if both graphical structures are identical
265 if (UndiGraph::operator!=(from)) return false;
266
267 // check if the _cliques_ are identical
268 for (const auto& [node, nodes]: _cliques_)
269 if (nodes != from._cliques_[node]) return false;
270
271 return true;
272 }
273
274 std::string CliqueGraph::toString() const {
275 std::stringstream stream;
276 stream << "list of nodes:" << std::endl;
277
278 for (const auto node: nodes()) {
279 stream << " -- node: " << node << std::endl << " clique:";
280
281 for (const auto cliq: clique(node))
282 stream << " " << cliq;
283
284 stream << std::endl;
285 }
286
287 stream << "\n\nlist of edges:\n";
288
289 for (const auto& edge: edges())
290 stream << edge << " ";
291
292 return stream.str();
293 }
294
295 std::string expandCliqueContent(const NodeSet& clique, const std::string& delim = "-") {
296 std::stringstream stream;
297 bool first = true;
298
299 std::vector< NodeId > sorted(clique.begin(), clique.end());
300 std::sort(sorted.begin(), sorted.end());
301 for (auto node: sorted) {
302 if (!first) { stream << delim; }
303
304 stream << node;
305 first = false;
306 }
307
308 return stream.str();
309 }
310
311 std::string expandCliqueTooltip(const NodeSet& clique) {
312 std::stringstream stream;
313 stream << "size : " << clique.size() << "\\n" << expandCliqueContent(clique, "\\n");
314 return stream.str();
315 }
316
317 std::string expandClique(const NodeId n, const NodeSet& clique) {
318 std::stringstream stream;
319 stream << '(' << n << ") " << expandCliqueContent(clique);
320 return stream.str();
321 }
322
323 std::string expandSeparator(const NodeId n1,
324 const NodeSet& clique1,
325 const NodeId n2,
326 const NodeSet& clique2) {
327 std::stringstream stream;
328 stream << expandClique(n1, clique1) << "^" << expandClique(n2, clique2);
329 return stream.str();
330 }
331
332 std::string CliqueGraph::toDot() const {
333 std::stringstream stream;
334 stream << "graph {" << std::endl;
335 stream << R"( node [style="filled", fontcolor="black"];)" << std::endl;
336
337 // cliques as nodes
338 for (auto node: nodes()) {
339 std::string nom = '"' + expandClique(node, clique(node)) + '"';
340 stream << " " << nom << " [label=\"" << expandCliqueContent(clique(node)) << "\",tooltip=\""
341 << expandCliqueTooltip(clique(node)) << R"(",fillcolor ="burlywood"];)" << std::endl;
342 }
343
344 stream << std::endl;
345
346 // separator as nodes
347 for (const auto& edge: edges()) {
348 stream << " \""
349 << expandSeparator(edge.first(),
350 clique(edge.first()),
351 edge.second(),
352 clique(edge.second()))
353 << "\" [label=\"" << expandCliqueContent(separator(edge)) << "\",tooltip=\""
354 << expandCliqueTooltip(separator(edge))
355 << R"(",shape=box,fillcolor="palegreen",fontsize=8,width=0,height=0];)" << std::endl;
356 }
357
358 stream << std::endl;
359
360 // edges now as c1--sep--c2
361 for (const auto& edge: edges())
362 stream << " \"" << expandClique(edge.first(), clique(edge.first())) << "\"--\""
363 << expandSeparator(edge.first(),
364 clique(edge.first()),
365 edge.second(),
366 clique(edge.second()))
367 << "\"--\"" << expandClique(edge.second(), clique(edge.second())) << "\";"
368 << std::endl;
369
370 stream << "}" << std::endl;
371
372 return stream.str();
373 }
374
375 std::string CliqueGraph::mapToDot(double scaleClique,
376 double scaleSep,
377 double lenEdge,
378 const std::string& colorClique,
379 const std::string& colorSep) const {
380 std::stringstream stream;
381 stream << "graph {" << std::endl;
382 stream << " bgcolor=transparent;" << std::endl;
383 stream << " layout=neato;" << std::endl << std::endl;
384 stream << " node [shape=point,style=filled, fillcolor =" << colorClique << "];" << std::endl;
385 stream << " edge [len=" << lenEdge << "];" << std::endl << std::endl;
386
387 // cliques as nodes
388 for (auto node: nodes()) {
389 const auto& clik = clique(node);
390 stream << " " << node << " [tooltip=\"" << expandCliqueTooltip(clik)
391 << "\", width=" << scaleClique * double(clik.size()) << "];" << std::endl;
392 }
393 stream << std::endl;
394 stream << " node [shape=square,style=filled, fillcolor =" << colorSep << ",label=\"\"];"
395 << std::endl
396 << std::endl;
397
398 // separator as nodes and edges
399 for (const auto& edge: edges()) {
400 // the separator as node
401 const auto sep = clique(edge.first()) * clique(edge.second());
402 stream << " \"" << edge.first() << "~" << edge.second() << "\" [tooltip=\""
403 << expandCliqueTooltip(sep) << "\""
404 << ", width=" << scaleSep * double(sep.size()) << "];" << std::endl;
405 // the edges
406 stream << " \"" << edge.first() << "\"--\"" << edge.first() << "~" << edge.second()
407 << "\"--\"" << edge.second() << "\";" << std::endl
408 << std::endl;
409 }
410
411 stream << "}" << std::endl;
412
413 return stream.str();
414 }
415
417
418 std::ostream& operator<<(std::ostream& stream, const CliqueGraph& graph) {
419 stream << graph.toString();
420 return stream;
421 }
422
423} /* namespace gum */
424
425#endif /* DOXYGEN_SHOULD_SKIP_THIS */
CliqueGraph(Size nodes_size=HashTableConst::default_size, bool nodes_resize_policy=true, Size edges_size=HashTableConst::default_size, bool edges_resize_policy=true)
basic constructor: creates an empty clique graph
Exception : a similar element already exists.
Class for node sets in graph.
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:539
void erase(const Key &k)
Erases an element from the set.
Definition set_tpl.h:582
Base class for undirected graphs.
Definition undiGraph.h:128
Basic class for all graphs of cliques (join trees, etc).
inline source of basic clique graphs
#define GUM_ERROR(type, msg)
Definition exceptions.h:72
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Definition types.h:74
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
std::ostream & operator<<(std::ostream &out, const TiXmlNode &base)
Definition tinyxml.cpp:1516