aGrUM 2.3.2
a C++ library for (probabilistic) graphical models
MeekRules.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 <agrum/agrum.h>
48
50
51namespace gum {
52
54 MeekRules::MeekRules() { GUM_CONSTRUCTOR(MeekRules); }
55
57 MeekRules::~MeekRules() { GUM_DESTRUCTOR(MeekRules); }
58
61 _choices_.clear();
62 return _propagates_(graph);
63 }
64
67 _choices_.clear();
68
69 MixedGraph graph = _propagates_(mg);
70
71 // Resolve double-headed arc while avoiding cycle creation.
73
74 // cycle have been resolved above, so we can safely convert to PDAG
75 PDAG pdag;
76 for (auto node: graph) {
77 pdag.addNodeWithId(node);
78 }
79 for (const Edge& edge: graph.edges()) {
80 pdag.addEdge(edge.first(), edge.second());
81 }
82 for (const Arc& arc: graph.arcs()) {
83 pdag.addArc(arc.tail(), arc.head());
84 }
85 return pdag;
86 }
87
90 _choices_.clear();
91 // Orient all remaining edges into arcs
92 MixedGraph graph = _propagates_(mg);
93
94 // complete remaining edges
95 _complete_(graph);
96
97 // Resolve double-headed arc while avoiding cycle creation.
99
100
101 DAG dag;
102 for (auto node: graph.nodes()) {
103 dag.addNodeWithId(node);
104 }
105 for (const Arc& arc: graph.arcs()) {
106 dag.addArc(arc.tail(), arc.head());
107 }
108 return dag;
109 }
110
113 gum::ArcSet L; // create a set of all double-headed arcs
114 for (gum::NodeId x: mg.nodes())
115 for (NodeId y: mg.parents(x))
116 // If there is a mutual parent-child relationship, add the arc to the set
117 if (mg.parents(y).contains(x)) {
118 if (x > y) {
119 continue; // Avoid duplicate arcs by considering only one direction
120 } else {
121 L.insert(gum::Arc(x, y));
122 }
123 }
124
125 // If there are double-headed arcs
126 if (not L.empty()) {
127 while (true) {
128 bool withdrawFlag_L = false;
129 for (auto arc: ArcSet(L)) {
130 bool tail_head = _existsDirectedPath_(mg, arc.tail(), arc.head());
131 bool head_tail = _existsDirectedPath_(mg, arc.head(), arc.tail());
132 bool withdrawFlag_arc = false;
133
134 // Case 1: There is already a path from tail to head and no path from head to tail
135 if (tail_head && !head_tail) {
136 // Erase the arc from head to tail to avoid cycles
137 mg.eraseArc(Arc(arc.head(), arc.tail()));
138 withdrawFlag_arc = true;
139
140 // Case 2: There is already a path from head to tail and no path from tail to head
141 } else if (!tail_head && head_tail) {
142 // Erase the arc from tail to head to avoid cycles
143 mg.eraseArc(Arc(arc.tail(), arc.head()));
144 withdrawFlag_arc = true;
145
146 // Case 3: There is no path between tail and head
147 } else if (!tail_head && !head_tail) {
148 // Choose an arbitrary orientation and erase the corresponding arc
149 // mg.eraseArc(Arc(arc.head(), arc.tail()));
150 mg.eraseArc(_critereMinParents_(mg, arc.tail(), arc.head()));
151 withdrawFlag_arc = true;
152 }
153
154 // Remove the arc from the set if it was processed
155 if (withdrawFlag_arc) {
156 L.erase(arc);
157 withdrawFlag_L = true;
158 }
159 }
160 // If all double-headed arcs are processed, exit the loop
161 if (L.empty()) { break; }
162
163 // If no arcs were withdrawn, erase an arbitrary double arc in the graph (the first one in
164 // L). Hoping the situation will improve. ┐( ̄ヘ ̄)┌ If we arrive here, it's because the
165 // double-headed arc creates cycles in both directions
166 if (!withdrawFlag_L) {
167 auto it = L.begin();
168 auto arc = *it;
169 mg.eraseArc(Arc(arc.head(), arc.tail()));
170 mg.eraseArc(Arc(arc.tail(), arc.head()));
171 L.erase(arc);
172 }
173 }
174 }
175 }
176
178 MixedGraph graph(mg);
179 // Propagates existing orientations thanks to Meek rules
180 bool newOrientation = true;
181 while (newOrientation) {
182 newOrientation = false;
183 for (NodeId x: graph.nodes()) {
184 if (!graph.parents(x).empty()) { newOrientation |= _applyMeekRules_(graph, x); }
185 }
186 }
187 return graph;
188 }
189
191 // Propagates existing orientations thanks to rules
192 bool newOrientation = true;
193 while (newOrientation) {
194 newOrientation = false;
195 for (NodeId x: graph.nodes()) {
196 if (!graph.parents(x).empty()) { newOrientation |= _applyMeekRules_(graph, x); }
197 }
198 }
199
200 // GUM_TRACE(graph.toDot())
201 // Orient remaining edges
203 }
204
207 bool res = false;
208 const auto neighbours = graph.neighbours(xj);
209 for (auto& xi: neighbours) {
210 bool i_j = _isOrientable_(graph, xi, xj);
211 bool j_i = _isOrientable_(graph, xj, xi);
212 if (i_j || j_i) {
213 // GUM_SL_EMIT(xi, xj, "Removing Edge", "line 660")
214 graph.eraseEdge(Edge(xi, xj));
215 res = true;
216 }
217 if (i_j) {
218 graph.eraseEdge(Edge(xi, xj));
219 graph.addArc(xi, xj);
220 _applyMeekRules_(graph, xj);
221 }
222 if (j_i) {
223 graph.eraseEdge(Edge(xi, xj));
224 graph.addArc(xj, xi);
225 _applyMeekRules_(graph, xi);
226 }
227 if (i_j && j_i) {
228 // GUM_TRACE(" + add arc (" << xi << "," << xj << ")")
229 _choices_.emplace_back(xi, xj);
230 }
231 }
232 return res;
233 }
234
237 // If the number of parents of x is less than the number of parents of y.
238 if (graph.parents(x).size() < graph.parents(y).size()) {
239 // If the number of parents of x is less than the number of parents of y.
240 // We want to keep y->x, so we return x->y for erasure.
241 return Arc(x, y);
242 } else if (graph.parents(x).size() > graph.parents(y).size()) {
243 return Arc(y, x);
244 } else { // If they have the same number of parents, we choose the one with less neighbours.
245 if (graph.neighbours(x).size() < graph.neighbours(y).size()) {
246 return Arc(x, y);
247 } else {
248 return Arc(y, x);
249 }
250 }
251 }
252
253 bool MeekRules::_isOrientable_(const MixedGraph& graph, NodeId xi, NodeId xj) const {
254 // no cycle
255 if (_existsDirectedPath_(graph, xj, xi)) {
256 // GUM_TRACE("cycle(" << xi << "-" << xj << ")")
257 return false;
258 }
259 // R1
260 if (!(graph.parents(xi) - graph.boundary(xj)).empty()) {
261 // GUM_TRACE("R1(" << xi << "-" << xj << ")")
262 return true;
263 }
264 // R2
265 if (_existsDirectedPath_(graph, xi, xj)) {
266 // GUM_TRACE("R2(" << xi << "-" << xj << ")")
267 return true;
268 }
269 // R3
270 int nbr = 0;
271 for (const auto p: graph.parents(xj)) {
272 if (!graph.mixedOrientedPath(xi, p).empty()) {
273 nbr += 1;
274 if (nbr == 2) {
275 // GUM_TRACE("R3(" << xi << "-" << xj << ")")
276 return true;
277 }
278 }
279 }
280 return false;
281 }
282
285 // then decide the orientation for remaining edges
286 while (!essentialGraph.edges().empty()) {
287 const auto& edge = *(essentialGraph.edges().begin());
288 NodeId root = edge.first();
289 Size size_children_root = essentialGraph.children(root).size();
290 NodeSet visited;
291 NodeSet stack{root};
292
293 // check the best root for the set of neighbours
294 while (!stack.empty()) {
295 NodeId next = *(stack.begin());
296 stack.erase(next);
297 if (visited.contains(next)) continue;
298 if (essentialGraph.children(next).size() > size_children_root) {
299 size_children_root = essentialGraph.children(next).size();
300 root = next;
301 }
302 for (const auto n: essentialGraph.neighbours(next))
303 if (!stack.contains(n) && !visited.contains(n)) stack.insert(n);
304 visited.insert(next);
305 }
306
307 // orientation now
308 visited.clear();
309 stack.clear();
310 stack.insert(root);
311 while (!stack.empty()) {
312 // GUM_TRACE("stack : " << stack)
313 NodeId next = *(stack.begin());
314 stack.erase(next);
315 // GUM_TRACE("next : " << next)
316 if (visited.contains(next)) continue;
317 const auto nei = essentialGraph.neighbours(next);
318 for (const auto n: nei) {
319 // GUM_TRACE("n : "<< n)
320 if (!stack.contains(n) && !visited.contains(n)) stack.insert(n);
321 // GUM_TRACE(" + amap reasonably orientation for " << n << "->" << next);
322 if (_applyMeekRules_(essentialGraph, next)) {
323 continue;
324 } else {
325 if (!essentialGraph.existsArc(next,
326 n)) { // Checking that we're not creating a
327 // doubly-oriented arc by adding a "random" arc.
328 essentialGraph.eraseEdge(Edge(n, next));
329 essentialGraph.addArc(n, next);
330 _choices_.emplace_back(n, next);
331 // GUM_TRACE(" + add arc (" << n << "->" << next << ")")
332 }
333 }
334 }
335 visited.insert(next);
336 }
337 }
338 }
339
340 bool MeekRules::_existsDirectedPath_(const MixedGraph& graph, const NodeId n1, const NodeId n2) {
341 // not recursive version => use a FIFO for simulating the recursion
342 List< NodeId > nodeFIFO;
343 // mark[node] = successor if visited, else mark[node] does not exist
344 Set< NodeId > mark;
345
346 mark.insert(n2);
347 nodeFIFO.pushBack(n2);
348
349 NodeId current;
350
351 while (!nodeFIFO.empty()) {
352 current = nodeFIFO.front();
353 nodeFIFO.popFront();
354
355 // check the parents
356 for (const auto new_one: graph.parents(current)) {
357 if (graph.existsArc(current,
358 new_one)) // if there is a double arc, pass
359 continue;
360
361 if (new_one == n1) { return true; }
362
363 if (mark.exists(new_one)) // if this node is already marked, do not
364 continue; // check it again
365
366 mark.insert(new_one);
367 nodeFIFO.pushBack(new_one);
368 }
369 }
370
371 return false;
372 }
373
374} // namespace gum
Class for Meek Rules.
bool existsArc(const Arc &arc) const
indicates whether a given arc exists
const NodeSet & parents(NodeId id) const
returns the set of nodes with arc ingoing to a given node
NodeSet children(const NodeSet &ids) const
returns the set of children of a set of nodes
virtual void eraseArc(const Arc &arc)
removes an arc from the ArcGraphPart
const ArcSet & arcs() const
returns the set of arcs stored within the ArcGraphPart
The base class for all directed edges.
Base class for dag.
Definition DAG.h:121
void addArc(NodeId tail, NodeId head) final
insert a new arc into the directed graph
Definition DAG_inl.h:63
virtual void addArc(const NodeId tail, const NodeId head)
insert a new arc into the directed graph
Definition diGraph_inl.h:55
virtual void eraseEdge(const Edge &edge)
removes an edge from the EdgeGraphPart
const EdgeSet & edges() const
returns the set of edges stored within the EdgeGraphPart
const NodeSet & neighbours(NodeId id) const
returns the set of node neighbours to a given node
The base class for all undirected edges.
Generic doubly linked lists.
Definition list.h:379
Val & front() const
Returns a reference to first element of a list, if any.
Definition list_tpl.h:1703
bool empty() const noexcept
Returns a boolean indicating whether the chained list is empty.
Definition list_tpl.h:1831
void popFront()
Removes the first element of a List, if any.
Definition list_tpl.h:1825
Val & pushBack(const Val &val)
Inserts a new element (a copy) at the end of the chained list.
Definition list_tpl.h:1488
MixedGraph _propagates_(const MixedGraph &graph)
PDAG propagateToCPDAG(const MixedGraph &mg)
Propagates the orientation of a MixedGraph (no double-headed arcs) and return a PDAG.
Definition MeekRules.cpp:66
gum::Arc _critereMinParents_(const gum::MixedGraph &graph, gum::NodeId x, gum::NodeId y)
When resolving double-headed arcs, prioritize selecting the option that minimizes the number of paren...
MixedGraph propagate(const MixedGraph &mg)
Propagates the orientation of a MixedGraph (no double-headed arcs) and return a PDAG.
Definition MeekRules.cpp:60
std::vector< Arc > _choices_
Definition MeekRules.h:100
static bool _existsDirectedPath_(const MixedGraph &graph, NodeId n1, NodeId n2)
bool _applyMeekRules_(MixedGraph &graph, NodeId xj)
Propagates the orientation from a node to its neighbours.
virtual ~MeekRules()
destructor
Definition MeekRules.cpp:57
void _complete_(MixedGraph &graph)
bool _isOrientable_(const gum::MixedGraph &graph, gum::NodeId xi, gum::NodeId xj) const
void _propagatesOrientationInChainOfRemainingEdges_(gum::MixedGraph &graph)
heuristic for remaining edges when everything else has been tried
void _orientDoubleHeadedArcs_(MixedGraph &mg)
Tells us if we can orient the edge xi - xj to xi -> xj.
DAG propagateToDAG(const MixedGraph &mg)
Propagates the orientation of a MixedGraph and completes it as a DAG.
Definition MeekRules.cpp:89
MeekRules()
default constructor
Definition MeekRules.cpp:54
Base class for mixed graphs.
Definition mixedGraph.h:146
NodeSet boundary(NodeId node) const
returns the set of node adjacent to a given node
std::vector< NodeId > mixedOrientedPath(NodeId node1, NodeId node2) const
returns a mixed edge/directed arc path from node1 to node2 in the arc/edge set
const NodeGraphPart & nodes() const
return *this as a NodeGraphPart
virtual void addNodeWithId(const NodeId id)
try to insert a node with the given id
Base class for partially directed acyclic graphs.
Definition PDAG.h:130
void addEdge(NodeId first, NodeId second) final
insert a new edge into the partially directed graph
Definition PDAG_inl.h:81
void addArc(NodeId tail, NodeId head) final
insert a new arc into the directed graph
Definition PDAG_inl.h:63
iterator begin() const
The usual unsafe begin iterator to parse the set.
Definition set_tpl.h:438
Size size() const noexcept
Returns the number of elements in the set.
Definition set_tpl.h:636
bool exists(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:533
bool contains(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:497
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:539
bool empty() const noexcept
Indicates whether the set is the empty set.
Definition set_tpl.h:642
void erase(const Key &k)
Erases an element from the set.
Definition set_tpl.h:582
void clear()
Removes all the elements, if any, from the set.
Definition set_tpl.h:338
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Definition types.h:74
Size NodeId
Type for node ids.
Set< Arc > ArcSet
Some typdefs and define for shortcuts ...
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
Definition agrum.h:46