aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
causalImpact_tpl.h
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#pragma once
43
44#include <algorithm>
45
47
48namespace gum {
49 // ---------- helpers ----------
50
51 inline std::string _formatVarsForSentence_(const Set< std::string >& vars) {
52 std::vector< std::string > names;
53 for (const auto& v: vars) {
54 names.push_back(v);
55 }
56 std::ranges::sort(names);
57
58 if (names.empty()) { return ""; }
59 if (names.size() == 1) { return names[0]; }
60 if (names.size() == 2) { return names[0] + " and " + names[1]; }
61
62 std::string os;
63 for (size_t i = 0; i < names.size(); ++i) {
64 if (i > 0) { os += (i + 1 == names.size() ? ", and " : ", "); }
65 os += names[i];
66 }
67 return os;
68 }
69
70 inline std::string _dsepExplanation_(const Set< std::string >& on,
71 const Set< std::string >& doing,
72 const Set< std::string >& knowing) {
73 std::string os = std::format("No causal effect of {} on {} because they are d-separated "
74 "after removing incoming edges into {}",
78 if (!knowing.empty()) {
79 os += std::format(" and conditioning on {}", _formatVarsForSentence_(knowing));
80 }
81 os += ".";
82 return os;
83 }
84
85 inline DAG _removeIncomingIntoLocal_(const DAG& g, const NodeSet& xset) {
86 DAG out(g);
87 for (const auto x: xset) {
88 const auto parents = out.parents(x);
89 for (const auto p: parents)
90 out.eraseArc(Arc(p, x));
91 }
92 return out;
93 }
94
95 template < GUM_Numeric GUM_SCALAR >
96 std::string _formatNodeSetNames_(const CausalModel< GUM_SCALAR >& cm, const NodeSet& zset) {
97 std::vector< std::string > names;
98 for (auto id: zset) {
99 names.push_back(cm.nameFromId(id));
100 }
101 std::ranges::sort(names);
102 std::string out = "[";
103 for (size_t i = 0; i < names.size(); ++i) {
104 if (i > 0) out += ", ";
105 out += "'" + names[i] + "'";
106 }
107 out += "]";
108 return out;
109 }
110
111 template < GUM_Numeric GUM_SCALAR >
114 const NodeSet& ids) {
116 for (auto id: ids)
117 out.insert(cm.nameFromId(id));
118 return out;
119 }
120
121 template < GUM_Numeric GUM_SCALAR >
122 typename gum::NodeSet
124 const Set< std::string >& names) {
125 NodeSet out;
126 for (const auto& n: names)
127 out.insert(cm.idFromName(n));
128 return out;
129 }
131 template < GUM_Numeric GUM_SCALAR >
133 const Set< std::string >& b,
135 if (std::any_of(a.begin(), a.end(), [&b, &c](const auto& x) {
136 return b.contains(x) || c.contains(x);
137 })) {
138 return false;
139 }
140 if (std::any_of(b.begin(), b.end(), [&c](const auto& x) { return c.contains(x); })) {
141 return false;
142 }
144 return true;
146
147 // ---------- ctors (initializer-list builds) ----------
148
149
150 template < GUM_Numeric GUM_SCALAR >
152 const Set< std::string >& on,
155 bool directDoCalculus) :
156 _directDoCalculus_{directDoCalculus},
157 _resultFormula_(_buildFromNames_(cm, on, doing, knowing, directDoCalculus)) {}
158
159 template < GUM_Numeric GUM_SCALAR >
161 const NodeSet& on,
162 const NodeSet& doing,
163 const NodeSet& knowing,
164 bool directDoCalculus) :
165 _directDoCalculus_{directDoCalculus},
166 _resultFormula_(_buildFromIds_(cm, on, doing, knowing, directDoCalculus)) {}
167
168 // ---------- builders ----------
169
170 template < GUM_Numeric GUM_SCALAR >
171 CausalFormula< GUM_SCALAR >
173 const Set< std::string >& on,
176 bool directDoCalculus) {
177 if (!_disjoint_(on, doing, knowing)) {
179 "The 3 parts of the query (on, doing, knowing) must be pairwise disjoint.");
180 }
181
182 const NodeSet i_on = _namesToIds_(cm, on);
183 const NodeSet i_doing = _namesToIds_(cm, doing);
184 const NodeSet i_knowing = _namesToIds_(cm, knowing);
185
186 NodeSet cond = i_knowing;
187
188 if (!directDoCalculus) {
189 // single-X, single-Y, empty-K: try BACKDOOR then FRONTDOOR
190 if (doing.size() == 1 && on.size() == 1 && knowing.empty()) {
191 const auto& Xname = *doing.begin();
192 const auto& Yname = *on.begin();
193 const auto Xid = cm.idFromName(Xname);
194 const auto Yid = cm.idFromName(Yname);
195
196 // --- Backdoor (Z may be empty; validate in G_{\underline X}) ---
197 {
198 auto optZ = cm.backDoor(Xid, Yid); // nullopt = no set found
199 if (optZ.has_value()
201 NodeSet{Xid},
203 *optZ)) {
204 NodeSet Z = *optZ;
206 auto ast = dc.getBackDoorTree(Xid, Yid, Z);
207 return CausalFormula< GUM_SCALAR >(cm,
208 std::move(ast),
209 on,
210 doing,
211 knowing,
212 std::string("backdoor ")
213 + _formatNodeSetNames_(cm, Z) + " found.");
214 }
215 }
216
217 // --- Frontdoor (no trivial FD; require non-empty mediator set) ---
218 {
219 auto optZ = cm.frontDoor(Xid, Yid);
220 if (optZ.has_value() && !optZ->empty()) {
221 NodeSet Z = *optZ;
222 DoCalculus< GUM_SCALAR > dc(cm);
223 auto ast = dc.getFrontDoorTree(Xid, Yid, Z);
224 return CausalFormula< GUM_SCALAR >(cm,
225 std::move(ast),
226 on,
227 doing,
228 knowing,
229 std::string("frontdoor ")
230 + _formatNodeSetNames_(cm, Z) + " found.");
231 }
232 }
233 }
234
235 // fallback: no causal effect in G_bar(X)
236 const DAG gXbar = _removeIncomingIntoLocal_(cm.causalDAG(), i_doing);
237 if (Separation::isDSeparated(gXbar, i_doing, i_on, cond)) {
238 // P(Y | K) in the observational BN
239 auto ast = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(cm.observationalBN(),
240 on,
241 knowing);
243 std::move(ast),
244 on,
245 doing,
246 knowing,
247 _dsepExplanation_(on, doing, knowing));
248 }
249 }
250
251 // general do-calculus (when available in your DoCalculus)
252 try {
254 std::unique_ptr< ASTtree< GUM_SCALAR > > ast
255 = knowing.empty()
256 ? dc.doCalculus(i_on, i_doing) // NodeSet overload
257 : dc.doCalculusWithObservation(i_on, i_doing, i_knowing); // NodeSet overload
258
260 std::move(ast),
261 on,
262 doing,
263 knowing,
264 "Identified via do-calculus (ID/IDC).");
265 } catch (const NotImplementedYet&) {
266 // Not identifiable with current methods: return a “null-AST” formula with a message.
268 cm,
269 std::unique_ptr< ASTtree< GUM_SCALAR > >{},
270 on,
271 doing,
272 knowing,
273 "Effect not identifiable with available methods (do-calculus not yet implemented).");
274 } catch (const HedgeException& e) {
276 std::unique_ptr< ASTtree< GUM_SCALAR > >{},
277 on,
278 doing,
279 knowing,
280 std::string("Effect not identifiable: ") + e.what());
281 }
282 }
283
284 template < GUM_Numeric GUM_SCALAR >
287 const NodeSet& on,
288 const NodeSet& doing,
289 const NodeSet& knowing,
290 bool directDoCalculus) {
291 // Reuse the names path to share logic/explanations
292 const auto onN = _idsToNames_(cm, on);
293 const auto doingN = _idsToNames_(cm, doing);
294 const auto knowingN = _idsToNames_(cm, knowing);
295 return _buildFromNames_(cm, onN, doingN, knowingN, directDoCalculus);
296 }
297
298 template < GUM_Numeric GUM_SCALAR >
299 Instantiation makeInstantiationFromValues(const Tensor< GUM_SCALAR >& tensor,
301 Instantiation I; // start empty (partial instantiation)
302
303 for (const auto& [k, v]: values) {
304 try {
305 const auto& var = tensor.variable(k); // skip if not in tensor
306 const gum::Idx idx = var.index(v); // skip if label unknown
307 I.add(var); // add this dim only
308 I.chgVal(var, idx); // fix its value
309 } catch (const gum::NotFound&) {
310 // variable or label not found: ignore (non-strict)
311 continue;
312 } catch (...) {
313 // any other issue: ignore
314 continue;
315 }
316 }
317 return I;
318 }
319
320 template < GUM_Numeric GUM_SCALAR >
321 std::tuple< CausalImpact< GUM_SCALAR >, Tensor< GUM_SCALAR >, std::string >
323 const Set< std::string >& on,
324 const Set< std::string >& doing,
325 const Set< std::string >& knowing,
327 // Delegate to the dedicated class.
328 CausalImpact< GUM_SCALAR > ci(cm, on, doing, knowing);
329
330 // Numeric value: empty tensor if not identifiable
331 Tensor< GUM_SCALAR > numeric;
332 if (ci.isIdentified()) { numeric = ci.eval(); }
333
334 // If specific values were provided, strictly slice the tensor to those values
335 if (!values.empty() && numeric.nbrDim() > 0) {
336 const auto I
337 = makeInstantiationFromValues(numeric, values); // throws if any key/label is invalid
338 numeric = numeric.extract(I); // keep only remaining free dimensions (if any)
339 }
340
341 // Always return the explanation the class computed
342 // Capture before moving ci: explanation() returns a const ref into ci internals
343 std::string expl = ci.explanation();
344 return std::make_tuple(std::move(ci), std::move(numeric), std::move(expl));
345 }
346
347 template < GUM_Numeric GUM_SCALAR >
348 Tensor< GUM_SCALAR > CausalImpact< GUM_SCALAR >::eval() const {
349 return _resultFormula_.eval();
350 }
351
352 template < GUM_Numeric GUM_SCALAR >
354 return _resultFormula_.toString();
355 }
356
357 template < GUM_Numeric GUM_SCALAR >
358 std::string CausalImpact< GUM_SCALAR >::toLatex(std::string_view doOperatorPrefix,
359 std::string_view doOperatorSuffix) const {
360 return _resultFormula_.toLatex(doOperatorPrefix, doOperatorSuffix);
361 }
362
363 template < GUM_Numeric GUM_SCALAR >
364 std::string CausalImpact< GUM_SCALAR >::latexQuery(std::string_view doOperatorPrefix,
365 std::string_view doOperatorSuffix) const {
366 return _resultFormula_.latexQuery(doOperatorPrefix, doOperatorSuffix);
367 }
368
369 template < GUM_Numeric GUM_SCALAR >
371 return _resultFormula_.isIdentified();
372 }
373
374 template < GUM_Numeric GUM_SCALAR >
375 const ASTtree< GUM_SCALAR >& CausalImpact< GUM_SCALAR >::root() const {
376 return _resultFormula_.root();
377 }
378
379 template < GUM_Numeric GUM_SCALAR >
380 const CausalFormula< GUM_SCALAR >& CausalImpact< GUM_SCALAR >::getResult() const {
381 return _resultFormula_;
382 }
383
384 template < GUM_Numeric GUM_SCALAR >
388
389 template < GUM_Numeric GUM_SCALAR >
391 return _resultFormula_.on();
392 }
393
394 template < GUM_Numeric GUM_SCALAR >
396 return _resultFormula_.doing();
397 }
398
399 template < GUM_Numeric GUM_SCALAR >
401 return _resultFormula_.knowing();
402 }
403
404 template < GUM_Numeric GUM_SCALAR >
405 const std::string& CausalImpact< GUM_SCALAR >::explanation() const {
406 return _resultFormula_.explanation();
407 }
408
409 template < GUM_Numeric GUM_SCALAR >
410 std::vector< std::string > CausalImpact< GUM_SCALAR >::onNames() const {
411 return _resultFormula_.onNames();
412 }
413
414 template < GUM_Numeric GUM_SCALAR >
415 std::vector< std::string > CausalImpact< GUM_SCALAR >::doingNames() const {
416 return _resultFormula_.doingNames();
417 }
418
419 template < GUM_Numeric GUM_SCALAR >
420 std::vector< std::string > CausalImpact< GUM_SCALAR >::knowingNames() const {
421 return _resultFormula_.knowingNames();
422 }
423
424} // namespace gum
Root abstract node for the AST of algebraic expressions.
Definition doAST.h:91
The base class for all directed edges.
A container for an identified causal query.
Builds a CausalFormula for a query (d-sep -> backdoor -> frontdoor -> (optional) do-calculus).
static CausalFormula< GUM_SCALAR > _buildFromIds_(const CausalModel< GUM_SCALAR > &cm, const NodeSet &on, const NodeSet &doing, const NodeSet &knowing, bool directDoCalculus)
static bool _disjoint_(const Set< std::string > &a, const Set< std::string > &b, const Set< std::string > &c)
CausalFormula< GUM_SCALAR > _resultFormula_
Identified symbolic formula. If not identifiable, its AST may be nullptr and its explanation string (...
const NodeSet & on() const
std::string toLatex(std::string_view doOperatorPrefix="do(", std::string_view doOperatorSuffix=")") const
Generates a full LaTeX equation: Query = Formula.
const CausalFormula< GUM_SCALAR > & getResult() const
Access the underlying CausalFormula result.
static Set< std::string > _idsToNames_(const CausalModel< GUM_SCALAR > &cm, const NodeSet &ids)
std::string toString() const
Generates a string representation of the formula's AST.
std::vector< std::string > doingNames() const
Tensor< GUM_SCALAR > eval() const
Evaluates the formula's AST to compute the resulting probability distribution.
const ASTtree< GUM_SCALAR > & root() const
Access the root AST node of the identified formula.
CausalImpact(const CausalModel< GUM_SCALAR > &cm, const Set< std::string > &on, const Set< std::string > &doing, const Set< std::string > &knowing=Set< std::string >(), bool directDoCalculus=false)
Constructs a CausalImpact object using variable names.
std::string latexQuery(std::string_view doOperatorPrefix="do(", std::string_view doOperatorSuffix=")") const
Generates a LaTeX representation of the original query, e.g., P(Y | do(X), Z).
const NodeSet & knowing() const
static CausalFormula< GUM_SCALAR > _buildFromNames_(const CausalModel< GUM_SCALAR > &cm, const Set< std::string > &on, const Set< std::string > &doing, const Set< std::string > &knowing, bool directDoCalculus)
static NodeSet _namesToIds_(const CausalModel< GUM_SCALAR > &cm, const Set< std::string > &names)
std::vector< std::string > onNames() const
Convenience: return names corresponding to stored node ids (sorted).
const NodeSet & doing() const
bool isIdentified() const noexcept
Whether the causal effect has been identified.
bool _directDoCalculus_
If true, skip backdoor/frontdoor and use do-calculus directly.
const std::string & explanation() const
std::vector< std::string > knowingNames() const
const CausalModel< GUM_SCALAR > & cm() const
A causal model pairing an observational BayesNet with a causal DAG.
Definition causalModel.h:84
DAG causalDAG() const
Causal DAG (observed + latent variables), with node names set.
std::optional< NodeSet > frontDoor(NodeId cause, NodeId effect) const
Find a frontdoor adjustment set (Z) between cause and effect (IDs only).
const BayesNet< GUM_SCALAR > & observationalBN() const
Observational BN (observed variables only).
Base class for dag.
Definition DAG.h:121
Instance-based do-calculus utilities bound to a single CausalModel.
Definition doCalculus.h:78
FormulaPtr doCalculusWithObservation(const NodeSet &on, const NodeSet &doing, const NodeSet &knowing) const
IDC: identify P(on | do(doing), knowing).
FormulaPtr getFrontDoorTree(NodeId cause, NodeId effect, const NodeSet &zset) const
FormulaPtr doCalculus(const NodeSet &on, const NodeSet &doing) const
Thin wrapper around ID: doCalculus(on, doing).
FormulaPtr getBackDoorTree(NodeId cause, NodeId effect, const NodeSet &zset) const
GUM_NODISCARD const char * what() const noexcept override
bool empty() const noexcept
Indicates whether the hash table is empty.
Class for assigning/browsing values to tuples of discrete variables.
Instantiation & chgVal(const DiscreteVariable &v, Idx newval)
Assign newval to variable v in the Instantiation.
void add(const DiscreteVariable &v) final
Adds a new variable in the Instantiation.
Exception: at least one argument passed to a function is not what was expected.
static bool isDSeparated(const DAG &dag, const NodeSet &X, const NodeSet &Y, const NodeSet &Z)
Test ( X \perp!!!\perp Y \mid Z ) (general d-separation).
static bool isBackdoorSeparated(const DAG &dag, const NodeSet &X, const NodeSet &Y, const NodeSet &Z)
Backdoor-style restriction: only paths whose first edge points into X.
static const iterator & end() noexcept
The usual unsafe end iterator to parse the set.
Definition set_tpl.h:421
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
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
Size Idx
Type for indexes.
Definition types.h:79
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
std::string _dsepExplanation_(const Set< std::string > &on, const Set< std::string > &doing, const Set< std::string > &knowing)
std::string _formatVarsForSentence_(const Set< std::string > &vars)
std::string _formatNodeSetNames_(const CausalModel< GUM_SCALAR > &cm, const NodeSet &zset)
DAG _removeIncomingIntoLocal_(const DAG &g, const NodeSet &xset)
Instantiation makeInstantiationFromValues(const Tensor< GUM_SCALAR > &tensor, const HashTable< std::string, std::string > &values)
Creates an instance for a tensor based on a HashTable.
std::tuple< CausalImpact< GUM_SCALAR >, Tensor< GUM_SCALAR >, std::string > causalImpact(const CausalModel< GUM_SCALAR > &cm, const Set< std::string > &on, const Set< std::string > &doing, const Set< std::string > &knowing=Set< std::string >(), const HashTable< std::string, std::string > &values=HashTable< std::string, std::string >())
Standalone helper that mirrors the Python function causalImpact(...).