aGrUM 3.1.1
a C++ library for (probabilistic) graphical models
gum::MeekRules Class Reference

Applies Meek's orientation rules to propagate arc directions in a mixed graph. More...

#include <MeekRules.h>

Public Member Functions

MixedGraph propagate (const MixedGraph &mg)
 Applies Meek rules to a MixedGraph and returns the partially oriented result.
PDAG propagateToCPDAG (const MixedGraph &mg)
 Applies Meek rules and returns a CPDAG (completed PDAG).
DAG propagateToDAG (const MixedGraph &mg)
 Applies Meek rules and completes the result into a DAG.
const std::vector< Arc > & choices () const
 Returns the arcs for which the algorithm made an arbitrary orientation choice.
Constructors / Destructors
 MeekRules ()
 default constructor
virtual ~MeekRules ()
 destructor

Private Member Functions

MixedGraph _propagates_ (const MixedGraph &graph)
 Applies Meek rules R1–R4 exhaustively and returns the resulting graph.
bool _applyMeekRules_ (MixedGraph &graph, NodeId xj)
 Tries to orient edges incident to xj using Meek rules R1–R4.
void _propagatesOrientationInChainOfRemainingEdges_ (MixedGraph &graph)
 Arbitrarily orients remaining undirected edges when no Meek rule applies.
void _complete_ (MixedGraph &graph)
 Orients all remaining undirected edges, applying Meek rules then a chain heuristic.

Static Private Member Functions

static void _orientDoubleHeadedArcs_ (MixedGraph &mg)
 Resolves double-headed arcs (x→y and y→x simultaneously) to avoid cycles.
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.
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).
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.

Private Attributes

std::vector< Arc_choices_

Detailed Description

Applies Meek's orientation rules to propagate arc directions in a mixed graph.

Given a MixedGraph (containing both directed arcs and undirected edges, with no double-headed arcs in the input), this class applies rules R1–R4 exhaustively to orient as many undirected edges as possible without introducing new v-structures. Three output flavours are available: MixedGraph (partial orientation), PDAG (completed PDAG / CPDAG), and DAG (fully oriented).

When the rules alone cannot resolve all undirected edges, an arbitrary orientation is chosen using a min-parents heuristic; the affected arcs are recorded and accessible via choices().

Definition at line 67 of file MeekRules.h.

Constructor & Destructor Documentation

◆ MeekRules()

gum::MeekRules::MeekRules ( )

default constructor

Definition at line 58 of file MeekRules.cpp.

58{ GUM_CONSTRUCTOR(MeekRules); }
MeekRules()
default constructor
Definition MeekRules.cpp:58

References MeekRules().

Referenced by MeekRules(), and ~MeekRules().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ ~MeekRules()

gum::MeekRules::~MeekRules ( )
virtual

destructor

Definition at line 61 of file MeekRules.cpp.

61{ GUM_DESTRUCTOR(MeekRules); }

References MeekRules().

Here is the call graph for this function:

Member Function Documentation

◆ _applyMeekRules_()

bool gum::MeekRules::_applyMeekRules_ ( MixedGraph & graph,
NodeId xj )
private

Tries to orient edges incident to xj using Meek rules R1–R4.

Propagates the orientation from a node to its neighbours.

Parameters
graphthe graph (modified in place)
xjthe node whose incident undirected edges are examined
Returns
true if at least one edge was oriented

Definition at line 212 of file MeekRules.cpp.

212 {
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);
225 _applyMeekRules_(graph, xj);
226 }
227 if (j_i) {
228 graph.addArc(xj, xi);
229 _applyMeekRules_(graph, 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 }
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.
std::vector< Arc > _choices_
Definition MeekRules.h:121
bool _applyMeekRules_(MixedGraph &graph, NodeId xj)
Tries to orient edges incident to xj using Meek rules R1–R4.

References _applyMeekRules_(), _choices_, and _isOrientable_().

Referenced by _applyMeekRules_(), _complete_(), _propagates_(), and _propagatesOrientationInChainOfRemainingEdges_().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _complete_()

void gum::MeekRules::_complete_ ( MixedGraph & graph)
private

Orients all remaining undirected edges, applying Meek rules then a chain heuristic.

Definition at line 196 of file MeekRules.cpp.

196 {
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 }
void _propagatesOrientationInChainOfRemainingEdges_(MixedGraph &graph)
Arbitrarily orients remaining undirected edges when no Meek rule applies.
Size NodeId
Type for node ids.

References _applyMeekRules_(), and _propagatesOrientationInChainOfRemainingEdges_().

Referenced by propagateToDAG().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _existsDirectedPath_()

bool gum::MeekRules::_existsDirectedPath_ ( const MixedGraph & graph,
NodeId n1,
NodeId n2 )
staticprivate

Returns true if there is a directed path from n1 to n2 (BFS, double arcs ignored).

Definition at line 361 of file MeekRules.cpp.

361 {
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 }

References gum::List< Val >::empty(), gum::Set< Key >::exists(), gum::List< Val >::front(), gum::Set< Key >::insert(), gum::List< Val >::popFront(), and gum::List< Val >::pushBack().

Referenced by _isOrientable_(), _orientDoubleHeadedArcs_(), and _propagatesOrientationInChainOfRemainingEdges_().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _isOrientable_()

bool gum::MeekRules::_isOrientable_ ( const MixedGraph & graph,
NodeId xi,
NodeId xj )
staticnodiscardprivate

Returns true if the edge xi–xj can be oriented as xi→xj under Meek rules R1–R3.

Parameters
graphthe graph
xicandidate tail
xjcandidate head

Definition at line 258 of file MeekRules.cpp.

258 {
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 }
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).

References _existsDirectedPath_().

Referenced by _applyMeekRules_().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _orientDoubleHeadedArcs_()

void gum::MeekRules::_orientDoubleHeadedArcs_ ( MixedGraph & mg)
staticprivate

Resolves double-headed arcs (x→y and y→x simultaneously) to avoid cycles.

Orient double-headed arcs while avoiding cycles.

For each double-headed arc, erases one direction based on existing directed paths; if neither direction creates a cycle, applies the min-parents heuristic.

Parameters
mgthe graph (modified in place)

Definition at line 116 of file MeekRules.cpp.

116 {
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 }
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.
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
Set< Arc > ArcSet
Some typdefs and define for shortcuts ...

References _existsDirectedPath_(), _selectArcToEraseByMinParents_(), gum::Set< Key >::begin(), gum::Set< Key >::contains(), gum::Set< Key >::empty(), gum::Set< Key >::erase(), gum::ArcGraphPart::eraseArc(), gum::Set< Key >::insert(), gum::NodeGraphPart::nodes(), and gum::ArcGraphPart::parents().

Referenced by propagateToCPDAG(), and propagateToDAG().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _propagates_()

MixedGraph gum::MeekRules::_propagates_ ( const MixedGraph & graph)
private

Applies Meek rules R1–R4 exhaustively and returns the resulting graph.

Definition at line 183 of file MeekRules.cpp.

183 {
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 }

References _applyMeekRules_().

Referenced by propagate(), propagateToCPDAG(), and propagateToDAG().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _propagatesOrientationInChainOfRemainingEdges_()

void gum::MeekRules::_propagatesOrientationInChainOfRemainingEdges_ ( MixedGraph & graph)
private

Arbitrarily orients remaining undirected edges when no Meek rule applies.

Arbitrary propagation if we can't propagate thanks to MeekRules.

Uses a BFS-based heuristic: picks the node with the most children as a local root and orients edges away from it.

Parameters
graphthe graph (modified in place)

Definition at line 302 of file MeekRules.cpp.

302 {
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 }
void clear()
Removes all the elements, if any, from the set.
Definition set_tpl.h:315
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 ...

References _applyMeekRules_(), _choices_, _existsDirectedPath_(), gum::DiGraph::addArc(), gum::Set< Key >::begin(), gum::ArcGraphPart::children(), gum::Set< Key >::clear(), gum::Set< Key >::contains(), gum::EdgeGraphPart::edges(), gum::Set< Key >::empty(), gum::Set< Key >::erase(), gum::EdgeGraphPart::eraseEdge(), gum::ArcGraphPart::existsArc(), gum::Set< Key >::insert(), gum::EdgeGraphPart::neighbours(), and gum::Set< Key >::size().

Referenced by _complete_().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _selectArcToEraseByMinParents_()

gum::Arc gum::MeekRules::_selectArcToEraseByMinParents_ ( const MixedGraph & graph,
gum::NodeId x,
gum::NodeId y )
staticprivate

Returns the arc to erase when resolving a double-headed arc, using a min-parents heuristic.

Prefers to keep the direction whose head already has more parents. Ties are broken by neighbor count.

Parameters
graphthe graph
xone endpoint
ythe other endpoint
Returns
the arc (x→y or y→x) that should be erased

Definition at line 239 of file MeekRules.cpp.

241 {
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 }

Referenced by _orientDoubleHeadedArcs_().

Here is the caller graph for this function:

◆ choices()

INLINE const std::vector< Arc > & gum::MeekRules::choices ( ) const

Returns the arcs for which the algorithm made an arbitrary orientation choice.

An arc is recorded here when no Meek rule could determine its direction and the algorithm had to pick one arbitrarily (e.g. to break a double-headed arc or to orient a residual undirected edge).

Returns
the list of arbitrarily chosen arcs, in order of selection

Definition at line 57 of file MeekRules_inl.h.

57{ return _choices_; }

References _choices_.

◆ propagate()

MixedGraph gum::MeekRules::propagate ( const MixedGraph & mg)

Applies Meek rules to a MixedGraph and returns the partially oriented result.

Propagates MeekRules in a MixedGraph.

Meek rules R1–R4 are applied exhaustively; remaining undirected edges are left as-is. Double-headed arcs may appear in the output if two rules fire in opposite directions on the same edge.

Parameters
mgthe mixed graph to orient (arcs and undirected edges, no double-headed arcs)
Returns
the partially oriented graph; may still contain undirected edges or double-headed arcs

Definition at line 64 of file MeekRules.cpp.

64 {
65 _choices_.clear();
66 return _propagates_(graph);
67 }
MixedGraph _propagates_(const MixedGraph &graph)
Applies Meek rules R1–R4 exhaustively and returns the resulting graph.

References _choices_, and _propagates_().

Here is the call graph for this function:

◆ propagateToCPDAG()

PDAG gum::MeekRules::propagateToCPDAG ( const MixedGraph & mg)

Applies Meek rules and returns a CPDAG (completed PDAG).

Propagates the orientation of a MixedGraph (no double-headed arcs) and return a PDAG.

Meek rules R1–R4 are applied exhaustively, then any double-headed arcs are resolved using a min-parents heuristic to avoid cycles. The result is a valid PDAG with no double-headed arcs.

Parameters
mgthe mixed graph to orient
Returns
a PDAG representing the essential graph

Definition at line 70 of file MeekRules.cpp.

70 {
71 _choices_.clear();
72
73 MixedGraph graph = _propagates_(mg);
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 }
static void _orientDoubleHeadedArcs_(MixedGraph &mg)
Resolves double-headed arcs (x→y and y→x simultaneously) to avoid cycles.

References _choices_, _orientDoubleHeadedArcs_(), _propagates_(), gum::PDAG::addArc(), gum::PDAG::addEdge(), and gum::NodeGraphPart::addNodeWithId().

Here is the call graph for this function:

◆ propagateToDAG()

DAG gum::MeekRules::propagateToDAG ( const MixedGraph & mg)

Applies Meek rules and completes the result into a DAG.

Propagates the orientation of a MixedGraph and return a DAG.

Meek rules R1–R4 are applied, remaining undirected edges are arbitrarily oriented, and double-headed arcs are resolved to ensure an acyclic result.

Parameters
mgthe mixed graph to orient
Returns
a DAG consistent with the orientations implied by the input

Definition at line 93 of file MeekRules.cpp.

93 {
94 _choices_.clear();
95 // Orient all remaining edges into arcs
96 MixedGraph graph = _propagates_(mg);
97
98 // complete remaining edges
99 _complete_(graph);
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 }
void _complete_(MixedGraph &graph)
Orients all remaining undirected edges, applying Meek rules then a chain heuristic.

References _choices_, _complete_(), _orientDoubleHeadedArcs_(), _propagates_(), gum::DAG::addArc(), and gum::NodeGraphPart::addNodeWithId().

Here is the call graph for this function:

Member Data Documentation

◆ _choices_

std::vector< Arc > gum::MeekRules::_choices_
private

The documentation for this class was generated from the following files: