aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
CIBasedLearning.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
42#include <algorithm>
43#include <numeric>
44#include <set>
45#include <vector>
46
49
50namespace gum {
51
52 namespace learning {
53
54 // ##########################################################################
55 // Constructors / Destructors
56 // ##########################################################################
57
60
62
64 ConstraintBasedLearning(std::move(from)), test_(from.test_), alpha_(from.alpha_),
65 maxCondSetSize_(from.maxCondSetSize_), stable_(from.stable_),
66 exhaustiveSepSet_(from.exhaustiveSepSet_), ucPriority_(from.ucPriority_),
67 sepSet_(std::move(from.sepSet_)) {
68 from.test_ = nullptr;
69 }
70
72 if (this != &from) {
74 test_ = from.test_;
75 alpha_ = from.alpha_;
77 stable_ = from.stable_;
80 sepSet_ = from.sepSet_;
81 }
82 return *this;
83 }
84
86 if (this != &from) {
88 test_ = from.test_;
89 alpha_ = from.alpha_;
90 maxCondSetSize_ = from.maxCondSetSize_;
91 stable_ = from.stable_;
92 exhaustiveSepSet_ = from.exhaustiveSepSet_;
93 ucPriority_ = from.ucPriority_;
94 sepSet_ = std::move(from.sepSet_);
95 from.test_ = nullptr;
96 }
97 return *this;
98 }
99
100 // ##########################################################################
101 // Parameterisation
102 // ##########################################################################
103
105
107
108 double CIBasedLearning::alpha() const { return alpha_; }
109
111
112 void CIBasedLearning::setStable(bool stable) { stable_ = stable; }
113
114 void CIBasedLearning::setExhaustiveSepSet(bool exhaustive) { exhaustiveSepSet_ = exhaustive; }
115
117
119
121
122 // ##########################################################################
123 // Phase 1 — Skeleton
124 // ##########################################################################
125
128 sepSet_.clear();
129
130 Size d = 0;
131 while (d <= maxCondSetSize_) {
132 std::vector< Edge > edges(graph.edges().begin(), graph.edges().end());
133
134 bool anyTested = false;
135 std::vector< Edge > toRemove;
136
137 for (const Edge& edge: edges) {
138 if (!graph.existsEdge(edge)) { continue; }
139
140 const NodeId X = edge.first();
141 const NodeId Y = edge.second();
142
143 [&]() {
144 if (d == 0) {
145 anyTested = true;
146 const double pval = test_->statistics(X, Y, _emptySet_).second;
147 if (pval <= alpha_) { return; }
148 if (stable_) {
149 toRemove.push_back(edge);
150 } else {
151 graph.eraseEdge(edge);
152 }
153 sepSet_.insert({X, Y}, {{}, pval});
154 sepSet_.insert({Y, X}, {{}, pval});
155 return;
156 }
157
158 bool exh_found = false;
159 std::set< NodeId > exh_union;
160 double exh_pval = 0.0;
161
162 for (int dir = 0; dir < 2; ++dir) {
163 const NodeId A = (dir == 0) ? X : Y;
164 const NodeId B = (dir == 0) ? Y : X;
165
166 std::vector< NodeId > adj;
167 for (NodeId nb: graph.neighbours(A)) {
168 if (nb != B) { adj.push_back(nb); }
169 }
170
171 if (static_cast< Size >(adj.size()) < d) { continue; }
172 anyTested = true;
173
174 std::vector< std::size_t > idx(d);
175 std::iota(idx.begin(), idx.end(), std::size_t(0));
176
177 for (;;) {
178 std::vector< NodeId > cond(d);
179 for (Size i = 0; i < d; ++i) {
180 cond[i] = adj[idx[i]];
181 }
182
183 const double pval = test_->statistics(A, B, cond).second;
184 if (pval > alpha_) {
185 if (!exhaustiveSepSet_) {
186 if (stable_) {
187 toRemove.push_back(edge);
188 } else {
189 graph.eraseEdge(edge);
190 }
191 sepSet_.insert({X, Y}, {cond, pval});
192 sepSet_.insert({Y, X}, {cond, pval});
193 return;
194 }
195 exh_found = true;
196 if (exh_pval == 0.0) { exh_pval = pval; }
197 for (const NodeId n: cond) {
198 exh_union.insert(n);
199 }
200 }
201
202 int i = static_cast< int >(d) - 1;
203 while (i >= 0 && idx[i] == adj.size() - d + static_cast< std::size_t >(i)) {
204 --i;
205 }
206 if (i < 0) { break; }
207 ++idx[i];
208 for (int j = i + 1; j < static_cast< int >(d); ++j) {
209 idx[j] = idx[j - 1] + 1;
210 }
211 }
212 }
213
214 if (exh_found) {
215 if (stable_) {
216 toRemove.push_back(edge);
217 } else {
218 graph.eraseEdge(edge);
219 }
220 const std::vector< NodeId > union_vec(exh_union.begin(), exh_union.end());
221 sepSet_.insert({X, Y}, {union_vec, exh_pval});
222 sepSet_.insert({Y, X}, {union_vec, exh_pval});
223 }
224 }();
225 }
226
227 if (stable_) {
228 for (const Edge& e: toRemove) {
229 if (graph.existsEdge(e)) { graph.eraseEdge(e); }
230 }
231 }
232
233 if (!anyTested) { break; }
234 ++d;
235 }
236
237 return graph;
238 }
239
240 // ##########################################################################
241 // Phase 2 — Unshielded collider orientation
242 // ##########################################################################
243
245 if (graph.existsEdge(src, dst)) {
246 if (isArcValid_(graph, src, dst) && !_existsDirectedPath_(graph, dst, src)) {
247 graph.eraseEdge(Edge(src, dst));
248 graph.addArc(src, dst);
249 GUM_SL_EMIT(src, dst, "Orient " << src << " -> " << dst, "V-structure collider");
250 } else {
251 resolveOrientConflict_(src, dst);
252 GUM_SL_EMIT(src,
253 dst,
254 "Conflict " << src << " -> " << dst,
255 "V-structure blocked by constraint");
256 }
257 } else if (graph.existsArc(dst, src)) {
258 resolveOrientConflict_(src, dst);
259 GUM_SL_EMIT(src, dst, "Conflict " << src << " -> " << dst, "V-structure conflict: latent?");
260 }
261 // existsArc(src, dst) → consistent, skip
262 }
263
265 _latentCouples_.push_back(Arc(src, dst));
266 }
267
269 _latentCouples_.clear();
270
271 const auto triples = unshieldedTriples_(graph);
272
273 const auto isCollider = [&](NodeId X, NodeId Y, NodeId Z) -> bool {
274 if (!sepSet_.exists({X, Y})) { return false; }
275 const SepSetEntry_& entry = sepSet_[{X, Y}];
276 return std::find(entry.cond.begin(), entry.cond.end(), Z) == entry.cond.end();
277 };
278
280 for (const ThreePoints& tp: triples) {
281 const NodeId X = std::get< 0 >(tp);
282 const NodeId Y = std::get< 1 >(tp);
283 const NodeId Z = std::get< 2 >(tp);
284 if (!isCollider(X, Y, Z)) { continue; }
287 }
288 } else {
289 struct Candidate {
290 NodeId X, Y, Z;
291 double pval;
292 };
293
294 std::vector< Candidate > candidates;
295 candidates.reserve(triples.size());
296
297 for (const ThreePoints& tp: triples) {
298 const NodeId X = std::get< 0 >(tp);
299 const NodeId Y = std::get< 1 >(tp);
300 const NodeId Z = std::get< 2 >(tp);
301 if (!isCollider(X, Y, Z)) { continue; }
302 candidates.push_back({X, Y, Z, sepSet_[{X, Y}].pval});
303 }
304
305 std::sort(candidates.begin(), candidates.end(), [](const Candidate& a, const Candidate& b) {
306 return a.pval > b.pval;
307 });
308
309 for (const auto& [X, Y, Z, pval]: candidates) {
312 }
313 }
314
315 return graph;
316 }
317
318 } /* namespace learning */
319
320} /* namespace gum */
Abstract base class for CI-test-based structure learning algorithms.
#define GUM_SL_EMIT(x, y, action, explain)
The base class for all directed edges.
The base class for all undirected edges.
Base class for mixed graphs.
Definition mixedGraph.h:146
Abstract base for CI-test-based causal structure learning (PC, FCI).
MixedGraph orientUnshieldedColliders_(MixedGraph graph)
orient unshielded colliders in graph and return the updated graph
void setAlpha(double alpha)
maximum conditioning set size; Size(-1) = unlimited (default)
UCPriority ucPriority() const
maximum conditioning set size; Size(-1) = unlimited (default)
void orientColliderArm_(MixedGraph &graph, NodeId src, NodeId dst)
orient one arm (src → dst) of a collider; calls resolveOrientConflict_ on conflict
bool exhaustiveSepSet() const
maximum conditioning set size; Size(-1) = unlimited (default)
void setIndependenceTest(IndependenceTest &test)
inject independence test (non-owning: caller manages lifetime)
void setUCPriority(UCPriority p)
collider candidate ordering (default: Standard)
UCPriority
Controls how collider candidates are ordered before orientation.
@ Standard
process triples in natural traversal order
CIBasedLearning & operator=(const CIBasedLearning &)
void setMaxCondSetSize(Size max_k)
maximum conditioning set size; Size(-1) = unlimited (default)
MixedGraph learnSkeleton(MixedGraph graph) override
Phase 1: skeleton discovery via conditional independence tests.
double alpha() const
maximum conditioning set size; Size(-1) = unlimited (default)
void setStable(bool stable)
stable mode: defer edge removals until end of each depth (default: true)
virtual void resolveOrientConflict_(NodeId src, NodeId dst)
conflict hook: called when orientation cannot proceed (arc blocked or reversed). Default: push Arc(sr...
HashTable< std::pair< NodeId, NodeId >, SepSetEntry_ > sepSet_
void setExhaustiveSepSet(bool exhaustive)
exhaustive sepset mode: accumulate union of all separating sets found at each depth,...
std::vector< ThreePoints > unshieldedTriples_(const MixedGraph &graph)
Builds a complete MixedGraph on the nodes of template_graph, minus edges forbidden by structural cons...
bool isArcValid_(const MixedGraph &graph, NodeId x, NodeId y)
static bool _existsDirectedPath_(const MixedGraph &graph, NodeId n1, NodeId n2)
Builds a complete MixedGraph on the nodes of template_graph, minus edges forbidden by structural cons...
ConstraintBasedLearning & operator=(const ConstraintBasedLearning &)
MixedGraph initGraph_(const MixedGraph &template_graph)
Builds a complete MixedGraph on the nodes of template_graph, minus edges forbidden by structural cons...
The base class for all the independence tests used for learning.
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Definition types.h:74
Size NodeId
Type for node ids.
Base classes for mixed directed/undirected graphs.
include the inlined functions if necessary
Definition CSVParser.h:55
std::tuple< NodeId, NodeId, NodeId > ThreePoints
gum is the global namespace for all aGrUM entities
Definition agrum.h:46