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