aGrUM 3.1.1
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-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 <agrum/agrum.h>
48
50
51#ifdef GUM_NO_INLINE
53#endif // GUM_NO_INLINE
54
55namespace gum {
56
58 MeekRules::MeekRules() { GUM_CONSTRUCTOR(MeekRules); }
59
61 MeekRules::~MeekRules() { GUM_DESTRUCTOR(MeekRules); }
62
68
71 _choices_.clear();
72
74
75 // Resolve double-headed arc while avoiding cycle creation.
77
78 // cycle have been resolved above, so we can safely convert to PDAG
79 PDAG pdag;
80 for (auto node: graph) {
81 pdag.addNodeWithId(node);
82 }
83 for (const Edge& edge: graph.edges()) {
84 pdag.addEdge(edge.first(), edge.second());
85 }
86 for (const Arc& arc: graph.arcs()) {
87 pdag.addArc(arc.tail(), arc.head());
88 }
89 return pdag;
90 }
91
94 _choices_.clear();
95 // Orient all remaining edges into arcs
97
98 // complete remaining edges
100
101 // Resolve double-headed arc while avoiding cycle creation.
103
104
105 DAG dag;
106 for (auto node: graph.nodes()) {
107 dag.addNodeWithId(node);
108 }
109 for (const Arc& arc: graph.arcs()) {
110 dag.addArc(arc.tail(), arc.head());
111 }
112 return dag;
113 }
114
117 gum::ArcSet L; // create a set of all double-headed arcs
118 for (gum::NodeId x: mg.nodes()) {
119 for (NodeId y: mg.parents(x)) {
120 // If there is a mutual parent-child relationship, add the arc to the set
121 if (mg.parents(y).contains(x)) {
122 if (x > y) {
123 continue; // Avoid duplicate arcs by considering only one direction
124 } else {
125 L.insert(gum::Arc(x, y));
126 }
127 }
128 }
129 }
130
131 // If there are double-headed arcs
132 if (!L.empty()) {
133 while (true) {
134 bool withdrawFlag_L = false;
135 for (auto& arc: ArcSet(L)) {
136 const bool tail_head = _existsDirectedPath_(mg, arc.tail(), arc.head());
137 const bool head_tail = _existsDirectedPath_(mg, arc.head(), arc.tail());
138 bool withdrawFlag_arc = false;
139
140 // Case 1: There is already a path from tail to head and no path from head to tail
141 if (tail_head && !head_tail) {
142 // Erase the arc from head to tail to avoid cycles
143 mg.eraseArc(Arc(arc.head(), arc.tail()));
144 withdrawFlag_arc = true;
145
146 // Case 2: There is already a path from head to tail and no path from tail to head
147 } else if (!tail_head && head_tail) {
148 // Erase the arc from tail to head to avoid cycles
149 mg.eraseArc(Arc(arc.tail(), arc.head()));
150 withdrawFlag_arc = true;
151
152 // Case 3: There is no path between tail and head
153 } else if (!tail_head && !head_tail) {
154 // Choose an arbitrary orientation and erase the corresponding arc
155 // mg.eraseArc(Arc(arc.head(), arc.tail()));
156 mg.eraseArc(_selectArcToEraseByMinParents_(mg, arc.tail(), arc.head()));
157 withdrawFlag_arc = true;
158 }
159
160 // Remove the arc from the set if it was processed
161 if (withdrawFlag_arc) {
162 L.erase(arc);
163 withdrawFlag_L = true;
164 }
165 }
166 // If all double-headed arcs are processed, exit the loop
167 if (L.empty()) { break; }
168
169 // If no arcs were withdrawn, erase an arbitrary double arc in the graph (the first one in
170 // L). Hoping the situation will improve. ┐( ̄ヘ ̄)┌ If we arrive here, it's because the
171 // double-headed arc creates cycles in both directions
172 if (!withdrawFlag_L) {
173 auto it = L.begin();
174 auto arc = *it;
175 mg.eraseArc(Arc(arc.head(), arc.tail()));
176 mg.eraseArc(Arc(arc.tail(), arc.head()));
177 L.erase(arc);
178 }
179 }
180 }
181 }
182
184 MixedGraph graph(mg);
185 // Propagates existing orientations thanks to Meek rules
186 bool newOrientation = true;
187 while (newOrientation) {
188 newOrientation = false;
189 for (NodeId x: graph.nodes()) {
190 if (!graph.parents(x).empty()) { newOrientation |= _applyMeekRules_(graph, x); }
191 }
192 }
193 return graph;
194 }
195
197 // Propagates existing orientations thanks to rules
198 bool newOrientation = true;
199 while (newOrientation) {
200 newOrientation = false;
201 for (NodeId x: graph.nodes()) {
202 if (!graph.parents(x).empty()) { newOrientation |= _applyMeekRules_(graph, x); }
203 }
204 }
205
206 // GUM_TRACE(graph.toDot())
207 // Orient remaining edges
209 }
210
213 bool res = false;
214 const auto neighbours = graph.neighbours(xj);
215 for (auto& xi: neighbours) {
216 bool i_j = _isOrientable_(graph, xi, xj);
217 bool j_i = _isOrientable_(graph, xj, xi);
218 if (i_j || j_i) {
219 // GUM_SL_EMIT(xi, xj, "Removing Edge", "line 660")
220 graph.eraseEdge(Edge(xi, xj));
221 res = true;
222 }
223 if (i_j) {
224 graph.addArc(xi, xj);
226 }
227 if (j_i) {
228 graph.addArc(xj, xi);
230 }
231 if (i_j && j_i) {
232 // GUM_TRACE(" + add arc (" << xi << "," << xj << ")")
233 _choices_.emplace_back(xi, xj);
234 }
235 }
236 return res;
237 }
238
240 gum::NodeId x,
241 gum::NodeId y) {
242 // If the number of parents of x is less than the number of parents of y.
243 if (graph.parents(x).size() < graph.parents(y).size()) {
244 // If the number of parents of x is less than the number of parents of y.
245 // We want to keep y->x, so we return x->y for erasure.
246 return {x, y};
247 } else if (graph.parents(x).size() > graph.parents(y).size()) {
248 return {y, x};
249 } else { // If they have the same number of parents, we choose the one with less neighbours.
250 if (graph.neighbours(x).size() < graph.neighbours(y).size()) {
251 return {x, y};
252 } else {
253 return {y, x};
254 }
255 }
256 }
257
259 // no cycle
260 if (_existsDirectedPath_(graph, xj, xi)) {
261 // GUM_TRACE("cycle(" << xi << "-" << xj << ")")
262 return false;
263 }
264 // R1
265 if (!(graph.parents(xi) - graph.boundary(xj)).empty()) {
266 // GUM_TRACE("R1(" << xi << "-" << xj << ")")
267 return true;
268 }
269 // R2
270 if (_existsDirectedPath_(graph, xi, xj)) {
271 // GUM_TRACE("R2(" << xi << "-" << xj << ")")
272 return true;
273 }
274 // R3: orient xi-xj if two non-adjacent parents of xj are both direct undirected
275 // neighbors of xi (classical Meek R3: chains xi-k->xj and xi-l->xj, k not adj l)
276 {
277 std::vector< NodeId > qualifying;
278 for (const auto p: graph.parents(xj)) {
279 if (graph.neighbours(xi).contains(p)) { qualifying.push_back(p); }
280 }
281 for (std::size_t i = 0; i < qualifying.size(); ++i)
282 for (std::size_t j = i + 1; j < qualifying.size(); ++j)
283 if (!graph.boundary(qualifying[i]).contains(qualifying[j])) {
284 // GUM_TRACE("R3(" << xi << "-" << xj << ")")
285 return true;
286 }
287 }
288 // R4: orient xi-xj if there is a purely directed path from xi to some node xk,
289 // xk is an undirected neighbor of xj, and xi is not adjacent to xk
290 for (const auto xk: graph.neighbours(xj)) {
291 if (graph.boundary(xi).contains(xk)) continue;
292 if (graph.existsArc(xi, xk) || graph.existsArc(xk, xi)) continue;
293 if (_existsDirectedPath_(graph, xi, xk)) {
294 // GUM_TRACE("R4(" << xi << "-" << xj << ")")
295 return true;
296 }
297 }
298 return false;
299 }
300
303 // then decide the orientation for remaining edges
304 while (!essentialGraph.edges().empty()) {
305 const auto& edge = *(essentialGraph.edges().begin());
306 NodeId root = edge.first();
307 Size size_children_root = essentialGraph.children(root).size();
308 NodeSet visited;
309 NodeSet stack{root};
310
311 // check the best root for the set of neighbours
312 while (!stack.empty()) {
313 NodeId next = *(stack.begin());
314 stack.erase(next);
315 if (visited.contains(next)) continue;
316 if (essentialGraph.children(next).size() > size_children_root) {
317 size_children_root = essentialGraph.children(next).size();
318 root = next;
319 }
320 for (const auto n: essentialGraph.neighbours(next))
321 if (!stack.contains(n) && !visited.contains(n)) stack.insert(n);
322 visited.insert(next);
323 }
324
325 // orientation now
326 visited.clear();
327 stack.clear();
328 stack.insert(root);
329 while (!stack.empty()) {
330 // GUM_TRACE("stack : " << stack)
331 NodeId next = *(stack.begin());
332 stack.erase(next);
333 // GUM_TRACE("next : " << next)
334 if (visited.contains(next)) continue;
335 const auto nei = essentialGraph.neighbours(next);
336 for (const auto n: nei) {
337 // GUM_TRACE("n : "<< n)
338 if (!stack.contains(n) && !visited.contains(n)) stack.insert(n);
339 // GUM_TRACE(" + amap reasonably orientation for " << n << "->" << next);
340 if (_applyMeekRules_(essentialGraph, next)) {
341 continue;
342 } else {
343 if (!essentialGraph.existsArc(next, n)) {
344 essentialGraph.eraseEdge(Edge(n, next));
345 if (!_existsDirectedPath_(essentialGraph, next, n)) {
346 essentialGraph.addArc(n, next);
347 _choices_.emplace_back(n, next);
348 } else {
349 // n→next would create a cycle; reverse is safe
350 essentialGraph.addArc(next, n);
351 _choices_.emplace_back(next, n);
352 }
353 }
354 }
355 }
356 visited.insert(next);
357 }
358 }
359 }
360
362 // not recursive version => use a FIFO for simulating the recursion
363 List< NodeId > nodeFIFO;
364 // mark[node] = successor if visited, else mark[node] does not exist
365 Set< NodeId > mark;
366
367 mark.insert(n2);
368 nodeFIFO.pushBack(n2);
369
370 NodeId current;
371
372 while (!nodeFIFO.empty()) {
373 current = nodeFIFO.front();
374 nodeFIFO.popFront();
375
376 // check the parents
377 for (const auto new_one: graph.parents(current)) {
378 if (graph.existsArc(current,
379 new_one)) // if there is a double arc, pass
380 continue;
381
382 if (new_one == n1) { return true; }
383
384 if (mark.exists(new_one)) // if this node is already marked, do not
385 continue; // check it again
386
387 mark.insert(new_one);
388 nodeFIFO.pushBack(new_one);
389 }
390 }
391
392 return false;
393 }
394
395} // namespace gum
Meek rules for orienting edges in a mixed graph.
Inline implementations of MeekRules.
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 nodes which consists in the node and its parents returns the set of children of a ...
virtual void eraseArc(const Arc &arc)
removes an arc from 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:75
void addArc(const NodeId tail, const NodeId head) override
insert a new arc into the directed graph
Definition diGraph_inl.h:59
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:378
Val & front() const
Returns a reference to first element of a list, if any.
Definition list_tpl.h:1694
Val & pushBack(const Val &val)
Inserts a new element (a copy) at the end of the chained list.
Definition list_tpl.h:1481
bool empty() const noexcept
Returns a boolean indicating whether the chained list is empty.
Definition list_tpl.h:1822
void popFront()
Removes the first element of a List, if any.
Definition list_tpl.h:1816
MixedGraph _propagates_(const MixedGraph &graph)
Applies Meek rules R1–R4 exhaustively and returns the resulting graph.
PDAG propagateToCPDAG(const MixedGraph &mg)
Applies Meek rules and returns a CPDAG (completed PDAG).
Definition MeekRules.cpp:70
static bool _isOrientable_(const MixedGraph &graph, NodeId xi, NodeId xj)
Returns true if the edge xi–xj can be oriented as xi→xj under Meek rules R1–R3.
MixedGraph propagate(const MixedGraph &mg)
Applies Meek rules to a MixedGraph and returns the partially oriented result.
Definition MeekRules.cpp:64
std::vector< Arc > _choices_
Definition MeekRules.h:121
static bool _existsDirectedPath_(const MixedGraph &graph, NodeId n1, NodeId n2)
Returns true if there is a directed path from n1 to n2 (BFS, double arcs ignored).
bool _applyMeekRules_(MixedGraph &graph, NodeId xj)
Tries to orient edges incident to xj using Meek rules R1–R4.
static Arc _selectArcToEraseByMinParents_(const MixedGraph &graph, NodeId x, NodeId y)
Returns the arc to erase when resolving a double-headed arc, using a min-parents heuristic.
virtual ~MeekRules()
destructor
Definition MeekRules.cpp:61
void _complete_(MixedGraph &graph)
Orients all remaining undirected edges, applying Meek rules then a chain heuristic.
static void _orientDoubleHeadedArcs_(MixedGraph &mg)
Resolves double-headed arcs (x→y and y→x simultaneously) to avoid cycles.
DAG propagateToDAG(const MixedGraph &mg)
Applies Meek rules and completes the result into a DAG.
Definition MeekRules.cpp:93
void _propagatesOrientationInChainOfRemainingEdges_(MixedGraph &graph)
Arbitrarily orients remaining undirected edges when no Meek rule applies.
MeekRules()
default constructor
Definition MeekRules.cpp:58
Base class for mixed graphs.
Definition mixedGraph.h:146
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:93
void addArc(NodeId tail, NodeId head) final
insert a new arc into the directed graph
Definition PDAG_inl.h:75
bool contains(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:468
bool exists(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:504
void clear()
Removes all the elements, if any, from the set.
Definition set_tpl.h:315
bool empty() const noexcept
Indicates whether the set is the empty set.
Definition set_tpl.h:613
iterator begin() const
The usual unsafe begin iterator to parse the set.
Definition set_tpl.h:409
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
Size size() const noexcept
Returns the number of elements in the set.
Definition set_tpl.h:607
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