106 if (!
sepSet_.exists({X, Y})) {
return false; }
108 return std::ranges::find(entry.
cond, Z) == entry.
cond.end();
113 const NodeId X = std::get< 0 >(tp);
114 const NodeId Y = std::get< 1 >(tp);
115 const NodeId Z = std::get< 2 >(tp);
116 if (!isCollider(X, Y, Z)) {
continue; }
130 std::vector< Candidate > candidates;
131 candidates.reserve(triples.size());
134 const NodeId X = std::get< 0 >(tp);
135 const NodeId Y = std::get< 1 >(tp);
136 const NodeId Z = std::get< 2 >(tp);
137 if (!isCollider(X, Y, Z)) {
continue; }
138 candidates.push_back({.X = X, .Y = Y, .Z = Z, .pval =
sepSet_[{X, Y}].pval});
141 std::ranges::sort(candidates,
142 [](
const Candidate& a,
const Candidate& b) {
return a.pval > b.pval; });
144 for (
const auto& [X, Y, Z, pval]: candidates) {
163 std::set< NodeId > visited{from};
164 std::queue< NodeId > q;
167 const NodeId cur = q.front();
169 if (cur == to) {
return true; }
171 if (visited.count(w) > 0) {
continue; }
182 bool uncoveredPdDFS_(
const PAG& pag,
184 std::vector< NodeId >& path,
185 std::set< NodeId >& visited) {
186 const NodeId cur = path.back();
188 for (std::size_t i = 0; i + 2 < path.size(); ++i) {
189 if (pag.existsEdge(path[i], path[i + 2])) {
return false; }
193 for (
const NodeId w: pag.neighbours(cur)) {
194 if (visited.count(w) > 0) {
continue; }
195 if (pag.isArrowhead(w, cur)) {
continue; }
196 if (pag.isTail(cur, w)) {
continue; }
199 if (uncoveredPdDFS_(pag, to, path, visited)) {
return true; }
208 std::set< NodeId > visited{from, next};
209 std::vector< NodeId > path{from, next};
210 return uncoveredPdDFS_(pag, to, path, visited);
216 void findUncoveredCirclePaths_(
const PAG& pag,
218 std::vector< NodeId >& path,
219 std::set< NodeId >& visited,
220 const std::vector< NodeId >& excludes,
221 std::vector< std::vector< NodeId > >& result) {
222 const NodeId cur = path.back();
224 bool uncovered =
true;
225 for (std::size_t i = 0; i + 2 < path.size() && uncovered; ++i) {
226 if (pag.existsEdge(path[i], path[i + 2])) { uncovered =
false; }
228 if (uncovered) { result.push_back(path); }
231 for (
const NodeId next: pag.neighbours(cur)) {
232 if (visited.count(next) > 0) {
continue; }
233 if (std::ranges::find(excludes, next) != excludes.end()) {
continue; }
234 if (!pag.isCircle(cur, next) || !pag.isCircle(next, cur)) {
continue; }
235 visited.insert(next);
236 path.push_back(next);
237 findUncoveredCirclePaths_(pag, to, path, visited, excludes, result);
249 std::set< NodeId > result;
250 std::queue< std::pair< NodeId, NodeId > > bfsQueue;
251 std::set< std::pair< NodeId, NodeId > > visited;
254 if (b == y) {
continue; }
255 if (visited.insert({x, b}).second) {
256 bfsQueue.emplace(x, b);
261 while (!bfsQueue.empty()) {
262 const auto [a, b] = bfsQueue.front();
266 if (c == x || c == y || c == a) {
continue; }
270 if (visited.insert({b, c}).second) {
271 bfsQueue.emplace(b, c);
274 if (existsSemiDirectedPath_(pag, c, x) || existsSemiDirectedPath_(pag, c, b)) {
284 return {result.begin(), result.end()};
291 const auto tryRemove = [&](
NodeId X,
NodeId Y,
const std::vector< NodeId >& dsep) {
293 ?
static_cast< Size >(dsep.size())
298 for (
Size k = 2; k <= maxK; ++k) {
299 if (dsep.size() < k) {
break; }
301 std::vector< std::size_t > idx(k);
302 std::iota(idx.begin(), idx.end(), std::size_t(0));
305 std::vector< NodeId > cond(k);
306 for (
Size i = 0; i < k; ++i) {
307 cond[i] = dsep[idx[i]];
310 const double pval =
test_->statistics(X, Y, cond).second;
318 if (k == 0) {
break; }
319 int i =
static_cast< int >(k) - 1;
321 && idx[
static_cast< std::size_t
>(i)]
322 == dsep.size() - k +
static_cast< std::size_t
>(i)) {
325 if (i < 0) {
break; }
326 ++idx[
static_cast< std::size_t
>(i)];
327 for (
int j = i + 1; j < static_cast< int >(k); ++j) {
328 idx[
static_cast< std::size_t
>(j)] = idx[
static_cast< std::size_t
>(j - 1)] + 1;
335 for (
const Edge& edge: edgeSnap) {
337 const NodeId X = edge.first();
338 const NodeId Y = edge.second();
341 if (tryRemove(X, Y, dsepXY)) {
continue; }
345 tryRemove(X, Y, dsepYX);
355 bool changed =
false;
360 if (c == a) {
continue; }
361 if (!pag.
isCircle(c, b)) {
continue; }
367 if (pag.
isTail(b, c)) {
continue; }
379 bool changed =
false;
383 if (a == c) {
continue; }
385 if (!pag.
isCircle(a, c)) {
continue; }
388 if (!pag.
isTail(b, a) && !pag.
isTail(c, b)) {
continue; }
393 if (pag.
isTail(a, c)) {
continue; }
406 bool changed =
false;
408 std::vector< NodeId > into_arrows, into_circles;
410 if (pag.
isArrowhead(n, b)) { into_arrows.push_back(n); }
411 if (pag.
isCircle(n, b)) { into_circles.push_back(n); }
413 if (into_arrows.size() < 2) {
continue; }
415 for (
const NodeId d: into_circles) {
416 for (std::size_t i = 0; i < into_arrows.size(); ++i) {
417 for (std::size_t j = i + 1; j < into_arrows.size(); ++j) {
418 const NodeId a = into_arrows[i];
419 const NodeId c = into_arrows[j];
423 if (!pag.
isCircle(a, d)) {
continue; }
424 if (!pag.
isCircle(c, d)) {
continue; }
426 bool nonCollider =
false;
427 for (
const auto& key: {std::make_pair(a, c), std::make_pair(c, a)}) {
429 const auto& cond =
sepSet_[key].cond;
430 if (std::ranges::find(cond, d) != cond.end()) {
436 if (!nonCollider) {
continue; }
450 bool changed =
false;
455 if (c == b) {
continue; }
459 if (d == a || d == c) {
continue; }
462 const std::vector< NodeId > excludes{a, b};
463 std::vector< std::vector< NodeId > > paths;
464 std::vector< NodeId > path{c};
465 std::set< NodeId > visited{c};
466 findUncoveredCirclePaths_(pag, d, path, visited, excludes, paths);
467 for (
const auto& p: paths) {
475 for (std::size_t k = 0; k + 1 < p.size(); ++k) {
490 bool changed =
false;
492 bool has_undirected =
false;
495 has_undirected =
true;
499 if (!has_undirected) {
continue; }
513 bool changed =
false;
515 std::vector< NodeId > a_list;
517 if (pag.
isCircle(a, b) && pag.
isTail(b, a)) { a_list.push_back(a); }
519 if (a_list.empty()) {
continue; }
521 if (!pag.
isCircle(c, b)) {
continue; }
522 for (
const NodeId a: a_list) {
523 if (a == c) {
continue; }
546 std::vector< NodeId > path;
548 while (previous.
exists(cur)) {
554 const double pval1 =
test_->statistics(d, c, path).second;
555 const bool ind1 = pval1 >
alpha_;
557 std::vector< NodeId > path_no_b;
558 for (
const NodeId n: path) {
559 if (n != b) { path_no_b.push_back(n); }
561 const double pval2 =
test_->statistics(d, c, path_no_b).second;
562 const bool ind2 = pval2 >
alpha_;
565 if (!ind1 && !ind2) {
567 const bool has_dc =
sepSet_.exists({d, c});
568 const bool has_cd =
sepSet_.exists({c, d});
569 if (!has_dc && !has_cd) {
return false; }
570 const auto& cond = has_dc ?
sepSet_[{d, c}].cond :
sepSet_[{c, d}].cond;
571 use_b_in_sep = (std::ranges::find(cond, b) != cond.end());
596 bool changed =
false;
600 std::vector< NodeId > poss_a;
602 if (pag.
isArrowhead(b, a)) { poss_a.push_back(a); }
605 std::vector< NodeId > poss_c;
610 for (
const NodeId a: poss_a) {
611 for (
const NodeId c: poss_c) {
612 if (a == c) {
continue; }
621 std::set< NodeId > c_parents;
626 std::queue< NodeId > q;
627 std::set< NodeId > visited{a, b};
635 Size cur_lvl_size = 1;
636 Size nxt_lvl_size = 0;
639 while (!q.empty() && !found) {
640 const NodeId t = q.front();
643 const NodeId prev_t = previous[t];
647 if (visited.count(d) > 0) {
continue; }
662 if (c_parents.count(d) > 0) {
671 if (cur_lvl_size == 0) {
676 cur_lvl_size = nxt_lvl_size;
694 bool changed =
false;
698 if (a == c) {
continue; }
701 if (!pag.
isCircle(c, a)) {
continue; }
703 if (!pag.
isTail(c, b)) {
continue; }
706 if (!case1 && !case2) {
continue; }
720 bool changed =
false;
724 if (!pag.
isCircle(c, a)) {
continue; }
725 bool oriented =
false;
727 if (b == c) {
continue; }
730 if (existsUncoveredPdPath_(pag, a, b, c)) {
737 if (oriented) {
continue; }
745 bool changed =
false;
748 std::vector< NodeId > into_c;
750 if (pag.
isArrowhead(n, c)) { into_c.push_back(n); }
752 if (into_c.size() < 2) {
continue; }
754 for (
const NodeId a: into_c) {
755 if (!pag.
isCircle(c, a)) {
continue; }
757 std::vector< NodeId > a_children;
759 if (x == c) {
continue; }
760 if (!pag.
isArrowhead(x, a)) { a_children.push_back(x); }
762 if (a_children.size() < 2) {
continue; }
764 bool oriented =
false;
765 for (std::size_t i = 0; i < into_c.size() && !oriented; ++i) {
766 for (std::size_t j = i + 1; j < into_c.size() && !oriented; ++j) {
767 const NodeId bd = into_c[i];
768 const NodeId dd = into_c[j];
769 if (!pag.
isTail(c, bd)) {
continue; }
770 if (!pag.
isTail(c, dd)) {
continue; }
771 for (std::size_t m = 0; m < a_children.size() && !oriented; ++m) {
772 for (std::size_t n2 = m + 1; n2 < a_children.size() && !oriented; ++n2) {
773 const NodeId ch1 = a_children[m];
774 const NodeId ch2 = a_children[n2];
780 const bool direct = existsSemiDirectedPath_(pag, ch1, bd)
781 && existsSemiDirectedPath_(pag, ch2, dd);
782 const bool crossed = existsSemiDirectedPath_(pag, ch1, dd)
783 && existsSemiDirectedPath_(pag, ch2, bd);
784 if (!direct && !crossed) {
continue; }
830 pag.
addEdge(e.first(), e.second());
833 for (
const Arc& a: skeleton.
arcs()) {
845 for (
const Arc& a: skeleton.
arcs()) {
FCI (Fast Causal Inference) causal discovery algorithm.
const ArcSet & arcs() const
returns the set of arcs stored within the ArcGraphPart
The base class for all directed edges.
const EdgeSet & edges() const
returns the set of edges stored within the EdgeGraphPart
bool existsEdge(const Edge &edge) const
indicates whether a given edge exists
const NodeSet & neighbours(NodeId id) const
returns the set of node neighbours to a given node
The base class for all undirected edges.
value_type & insert(const Key &key, const Val &val)
Adds a new element (actually a copy of this element) into the hash table.
void set(const Key &key, const Val &default_value)
Add a new property or modify it if it already existed.
bool exists(const Key &key) const
Checks whether there exists an element with a given key in the hashtable.
Base class for mixed graphs.
const NodeGraphPart & nodes() const
return *this as a NodeGraphPart
virtual void addNodeWithId(const NodeId id)
try to insert a node with the given id
Partial Ancestral Graph: undirected topology with endpoint marks.
bool isCircle(NodeId src, NodeId dst) const
true if mark at dst (from src) is Circle
bool isBidirected(NodeId x, NodeId y) const
true if edge x-y is bidirected x↔y (Arrowhead on both endpoints)
bool isDefCollider(NodeId x, NodeId z, NodeId y) const
true if z is a definite collider on path x-z-y (Arrowhead at z from both sides)
void reorientAllWith(EdgeMark m)
set all endpoint marks to m (used between FCI phases to reset to Circle)
void addEdge(NodeId x, NodeId y) override
add edge with Circle-Circle marks
void setMarkAt(NodeId src, NodeId dst, EdgeMark m)
set mark at dst-endpoint when traversing from src
bool isTail(NodeId src, NodeId dst) const
true if mark at dst (from src) is Tail
void eraseEdge(const Edge &e) override
remove edge and its two mark entries
bool isArrowhead(NodeId src, NodeId dst) const
true if mark at dst (from src) is Arrowhead
MixedGraph toMixedGraph() const
approximate conversion for learnDAG/learnPDAG: Tail-Arrowhead → arc; Arrowhead-Arrowhead → two arcs; ...
static const iterator & end() noexcept
The usual unsafe end iterator to parse the set.
iterator begin() const
The usual unsafe begin iterator to parse the set.
@ Standard
process triples in natural traversal order
CIBasedLearning & operator=(const CIBasedLearning &)
MixedGraph learnSkeleton(MixedGraph graph) override
Phase 1: skeleton discovery via conditional independence tests.
HashTable< std::pair< NodeId, NodeId >, SepSetEntry_ > sepSet_
std::vector< ThreePoints > unshieldedTriples_(const MixedGraph &graph)
Builds a complete MixedGraph on the nodes of template_graph, minus edges forbidden by structural cons...
bool isForbiddenArc_(NodeId x, NodeId y) const
void orientDoubleHeadedArcs_(MixedGraph &mg)
Builds a complete MixedGraph on the nodes of template_graph, minus edges forbidden by structural cons...
void applyStructuralConstraints_(MixedGraph &graph)
Builds a complete MixedGraph on the nodes of template_graph, minus edges forbidden by structural cons...
std::vector< Arc > _latentCouples_
PAG learnPAG(MixedGraph graph)
primary FCI output: learn a PAG from the given node-only or partial graph
std::vector< NodeId > possibleDSep(const PAG &pag, NodeId x, NodeId y) const
learnSkeleton() inherited from CIBasedLearning
bool doDdpOrientation_(PAG &pag, NodeId d, NodeId a, NodeId b, NodeId c, const HashTable< NodeId, NodeId > &previous) const
R4 helper: orient B,C in a detected discriminating path D..A,B,C.
bool ruleR7_(PAG &pag) const
tail propagation from A-oB
bool ruleR1_(PAG &pag) const
away from collider
FCI & operator=(const FCI &)
Size maxPathLength() const
maximum path length for R4 discriminating-path search; Size(-1) = unlimited
bool ruleR3_(PAG &pag) const
double triangle
std::vector< NodeId > computePossibleDSep_(const PAG &pag, NodeId x, NodeId y) const
Phase 3: return all nodes on possible-d-sep paths from x toward y (excl. x, y).
bool ruleR5_(PAG &pag) const
uncovered circle path → undirected
void orientCollidersOnPAG_(PAG &pag, const MixedGraph &topology)
Phase 2 / Phase 4: orient unshielded colliders directly on the PAG. topology is used for unshielded-t...
bool ruleR9_(PAG &pag) const
away from ancestor via uncovered pd-path
bool ruleR6_(PAG &pag) const
tail propagation from undirected edge
void applyOrientationRules_(PAG &pag) const
Phase 5: fixed-point loop over all orientation rules R1–R10.
bool ruleR8_(PAG &pag) const
away from ancestor (graph only)
void resolveOrientConflict_(NodeId src, NodeId dst) override
conflict hook override: FCI leaves circle marks — orientation conflicts are not recorded as latent co...
void possibleDSepPhase_(PAG &pag)
Phase 3: prune PAG edges using possibleDSep conditioning sets.
bool ruleR2_(PAG &pag) const
away from cycle
bool ruleR4_(PAG &pag) const
discriminating path (uses CI test)
MixedGraph learnMixedStructure(MixedGraph graph) override
bridge for learnDAG/learnPDAG: runs learnPAG then converts via toMixedGraph()
bool ruleR10_(PAG &pag) const
away from ancestor via two semi-directed paths
void setMaxPathLength(Size maxLen)
maximum path length for R4 discriminating-path search; Size(-1) = unlimited
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Size NodeId
Type for node ids.
include the inlined functions if necessary
std::tuple< NodeId, NodeId, NodeId > ThreePoints
std::vector< NodeId > cond