aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
doCalculus_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
50#pragma once
51
52#include <algorithm>
53#include <queue>
54#include <utility>
55
59
60#include <unordered_map>
61#include <unordered_set>
62
63namespace gum {
64
65
66 namespace {
67 // format a vector of strings as "{a, b, c}"
68 inline std::string _fmtBraceList_(std::vector< std::string > v) {
69 std::sort(v.begin(), v.end());
70 std::string os = "{";
71 for (size_t i = 0; i < v.size(); ++i) {
72 if (i) os += ", ";
73 os += v[i];
74 }
75 os += "}";
76 return os;
77 }
78
79 } // anonymous namespace
80
81 template < GUM_Numeric GUM_SCALAR >
83 return _cm;
84 }
85
86 /* ========================================================================== */
87 /* Backdoor / Frontdoor */
88 /* ========================================================================== */
89
90 template < GUM_Numeric GUM_SCALAR >
93 NodeId effect,
94 const NodeSet& zset) const {
95 // Preconditions: cause/effect must be observed (non-latent), Z must not contain latents.
96 const NodeSet lat = _cm.latentVariablesIds();
97 if (lat.contains(cause) || lat.contains(effect)) {
99 "getBackDoorTree: 'cause' and 'effect' must be observed (non-latent).");
100 }
101 for (auto z: zset) {
102 if (lat.contains(z)) {
103 GUM_ERROR(InvalidArgument, "getBackDoorTree: zset contains a latent variable.");
104 }
105 }
106
107 // ids -> names
108 const auto& dag = _cm.causalDAG();
109 auto mapO = _cm.id2name(/*includeLatentVariables=*/false);
110 const std::string xName = _cm.nameFromId(cause);
111 const std::string yName = _cm.nameFromId(effect);
112
113 Set< std::string > zNames;
114 std::vector< std::string > zOrder;
115 zOrder.reserve(zset.size());
116 for (auto z: zset) {
117 const std::string zn = _cm.nameFromId(z);
118 zNames.insert(zn);
119 zOrder.push_back(zn);
120 }
121 std::sort(zOrder.begin(), zOrder.end());
122
123 // P(y | x, z)
125 lhsY.insert(yName);
127 knw.insert(xName);
128 for (const auto& zn: zNames)
129 knw.insert(zn);
130 auto Pyxz = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(dag, mapO, lhsY, knw);
131
132 if (zNames.empty()) {
133 // returns P(y | x)
134 return Pyxz;
135 }
136
137 // P(z)
138 auto Pz = std::make_unique< ASTjointProba< GUM_SCALAR > >(zNames);
139
140 // P(y|x,z) * P(z), then sum over Z
141 std::vector< std::unique_ptr< ASTtree< GUM_SCALAR > > > terms;
142 terms.emplace_back(std::move(Pyxz));
143 terms.emplace_back(std::move(Pz));
144 auto prod = productOfTrees< GUM_SCALAR >(std::move(terms));
145
146 return std::make_unique< ASTsum< GUM_SCALAR > >(zOrder, std::move(prod));
147 }
148
149 template < GUM_Numeric GUM_SCALAR >
152 std::string_view effectName,
153 const NameList& zNames) const {
154 const NodeId x = _cm.idFromName(causeName);
155 const NodeId y = _cm.idFromName(effectName);
156
157 NodeSet Z;
158 for (const auto& zn: zNames)
159 Z.insert(_cm.idFromName(zn));
160
161 return getBackDoorTree(x, y, Z);
162 }
163
164 template < GUM_Numeric GUM_SCALAR >
167 NodeId effect,
168 const NodeSet& zset) const {
169 // --- Preconditions ---
170 const NodeSet lat = _cm.latentVariablesIds();
171 if (lat.contains(cause) || lat.contains(effect)) {
173 "getFrontDoorTree: 'cause' and 'effect' must be observed (non-latent).");
174 }
175 if (zset.empty()) {
176 GUM_ERROR(InvalidArgument, "getFrontDoorTree: zset must be non-empty for frontdoor.");
177 }
178 if (zset.contains(cause) || zset.contains(effect)) {
179 GUM_ERROR(InvalidArgument, "getFrontDoorTree: zset must be disjoint from {cause,effect}.");
180 }
181 for (auto z: zset) {
182 if (lat.contains(z)) {
183 GUM_ERROR(InvalidArgument, "getFrontDoorTree: zset contains a latent variable.");
184 }
185 }
186
187 // --- ids → names ---
188 const std::string xName = _cm.nameFromId(cause);
189 const std::string yName = _cm.nameFromId(effect);
190
191 Set< std::string > zNames;
192 std::vector< std::string > zOrder;
193 zOrder.reserve(zset.size());
194 for (auto z: zset) {
195 const auto zn = _cm.nameFromId(z);
196 zNames.insert(zn);
197 zOrder.push_back(zn);
198 }
199 std::sort(zOrder.begin(), zOrder.end()); // deterministic
200
201 // --- Build P(Z | X) using DAG ctor (observed-only mapping inferred from model) ---
202 const auto& dag = _cm.causalDAG();
203 auto mapO = _cm.id2name(/*includeLatentVariables=*/false);
204
205 Set< std::string > condX;
206 condX.insert(xName);
207 auto Pz_given_x = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(dag, mapO, zNames, condX);
208
209 // --- Build sum_x P(Y | X, Z) * P(X) ---
211 ySet.insert(yName);
212
213 Set< std::string > condXZ = zNames; // copy
214 condXZ.insert(xName);
215 auto Py_given_xz = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(dag, mapO, ySet, condXZ);
216
218 xSet.insert(xName);
219 auto Px = std::make_unique< ASTjointProba< GUM_SCALAR > >(xSet);
220
221 auto inner_prod
222 = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(Py_given_xz), std::move(Px));
223 auto inner_sum = std::make_unique< ASTsum< GUM_SCALAR > >(std::vector< std::string >{xName},
224 std::move(inner_prod));
225
226 // --- P(Z|X) * (sum_x ...) then sum over Z ---
227 auto outer_prod
228 = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(Pz_given_x), std::move(inner_sum));
229 return std::make_unique< ASTsum< GUM_SCALAR > >(zOrder, std::move(outer_prod));
230 }
231
232 template < GUM_Numeric GUM_SCALAR >
235 std::string_view effectName,
236 const NameList& zNames) const {
237 const NodeId x = _cm.idFromName(causeName);
238 const NodeId y = _cm.idFromName(effectName);
239
240 NodeSet Z;
241 for (const auto& zn: zNames)
242 Z.insert(_cm.idFromName(zn));
243
244 return getFrontDoorTree(x, y, Z);
245 }
246
247 /* ========================================================================== */
248 /* Helper utilities */
249 /* ========================================================================== */
250
251 template < GUM_Numeric GUM_SCALAR >
253 const NodeSet& Y,
254 const NodeSet& X,
255 const NodeSet& V,
256 const NodeSet& S) {
257 // turn NodeSets into sorted name lists
258 auto namesOf = [&](const NodeSet& Ns) {
259 std::vector< std::string > out;
260 out.reserve(Ns.size());
261 for (auto id: Ns)
262 out.push_back(cm.nameFromId(id));
263 std::sort(out.begin(), out.end());
264 return out;
265 };
266
267 const auto yN = namesOf(Y);
268 const auto xN = namesOf(X);
269 const auto vN = namesOf(V);
270 const auto sN = namesOf(S);
271
272 return std::format("Not identifiable: hedge detected. "
273 "Target P{} | do{}. Witness: V = {} is a single c-component and S = {} "
274 "is a c-component in both G and G\\X (forms a hedge). "
275 "Interpretation: unblocked confounding remains under intervention; "
276 "the effect cannot be expressed from the observational distribution "
277 "using current graph structure.",
278 _fmtBraceList_(yN),
279 _fmtBraceList_(xN),
280 _fmtBraceList_(vN),
281 _fmtBraceList_(sN));
282 }
283
284 template < GUM_Numeric GUM_SCALAR >
285 std::vector< NodeSet >
287 // c-components: undirected connectivity among observed children of the same latent.
288 const NodeSet lat = cm.latentVariablesIds();
289 const NodeSet V = cm.observationalBN().internalDag().nodes().asNodeSet();
290
291 // adjacency among observed nodes, via latent co-children
292 std::unordered_map< NodeId, std::vector< NodeId > > adj;
293 for (auto v: V)
294 adj[v]; // ensure key exists
295
296 for (auto h: lat) {
297 const auto& ch = cm.children(h);
298 // connect all observed pairs of children with an undirected edge
299 for (auto it1 = ch.begin(); it1 != ch.end(); ++it1) {
300 if (lat.contains(*it1)) continue;
301 auto it2 = it1;
302 ++it2;
303 for (; it2 != ch.end(); ++it2) {
304 if (lat.contains(*it2)) continue;
305 adj[*it1].push_back(*it2);
306 adj[*it2].push_back(*it1);
307 }
308 }
309 }
310
311 // BFS/DFS over observed nodes
312 std::vector< NodeSet > comps;
313 std::unordered_set< NodeId > seen;
314 for (auto v: V) {
315 if (seen.count(v)) continue;
316 NodeSet comp;
317 std::queue< NodeId > q;
318 q.push(v);
319 seen.insert(v);
320 comp.insert(v);
321 while (!q.empty()) {
322 auto u = q.front();
323 q.pop();
324 for (auto w: adj[u]) {
325 if (!seen.count(w)) {
326 seen.insert(w);
327 comp.insert(w);
328 q.push(w);
329 }
330 }
331 }
332 comps.push_back(std::move(comp));
333 }
334 return comps;
335 }
336
337 template < GUM_Numeric GUM_SCALAR >
338 std::vector< NodeSet >
342
343 template < GUM_Numeric GUM_SCALAR >
344 std::vector< NodeId >
346 const auto& dag = cm.observationalBN().internalDag();
347 const NodeSet V = dag.nodes().asNodeSet();
348
349 // indegree in the observed DAG
350 std::unordered_map< NodeId, Size > indeg;
351 for (auto v: V)
352 indeg[v] = dag.parents(v).size();
353
354 std::vector< NodeId > order;
355 order.reserve(V.size());
356
357 // Kahn on observed DAG only
358 std::queue< NodeId > q;
359 for (const auto& kv: indeg)
360 if (kv.second == 0) q.push(kv.first);
361
362 while (!q.empty()) {
363 const NodeId u = q.front();
364 q.pop();
365 order.push_back(u);
366
367 for (auto c: dag.children(u)) {
368 if (indeg[c] > 0) {
369 --indeg[c];
370 if (indeg[c] == 0) q.push(c);
371 }
372 }
373 }
374 return order;
375 }
376
377 template < GUM_Numeric GUM_SCALAR >
379 NodeSet res = T * g.nodes().asNodeSet(); // keep only nodes that actually exist in g
380
381 std::queue< NodeId > q;
382 for (auto t: res)
383 q.push(t);
384
385 while (!q.empty()) {
386 NodeId u = q.front();
387 q.pop();
388 for (auto p: g.parents(u)) {
389 if (!res.contains(p)) {
390 res.insert(p);
391 q.push(p);
392 }
393 }
394 }
395 return res;
396 }
397
398 template < GUM_Numeric GUM_SCALAR >
400 DAG g = dag; // copy
401 for (auto x: X) {
402 auto ps = g.parents(x); // snapshot
403 for (auto p: ps)
404 g.eraseArc(Arc(p, x));
405 }
406 return g;
407 }
408
409 template < GUM_Numeric GUM_SCALAR >
411 const NodeSet& doing,
412 const NodeSet& knowing) {
413 DAG g = dag; // copy
414 for (auto x: doing) {
415 auto ps = g.parents(x);
416 for (auto p: ps)
417 g.eraseArc(Arc(p, x));
418 }
419 for (auto z: knowing) {
420 auto ch = g.children(z);
421 for (auto c: ch)
422 g.eraseArc(Arc(z, c));
423 }
424 return g;
425 }
426
427 /* ========================================================================== */
428 /* General identification */
429 /* ========================================================================== */
430
431 template < GUM_Numeric GUM_SCALAR >
434 return _ID_(_cm, Y, X, nullptr);
435 }
436
437 template < GUM_Numeric GUM_SCALAR >
440 const NameList& X) const {
441 NodeSet YY, XX;
442 for (const auto& s: Y)
443 YY.insert(_cm.idFromName(s));
444 for (const auto& s: X)
445 XX.insert(_cm.idFromName(s));
446 return identifyingIntervention(YY, XX);
447 }
448
449 template < GUM_Numeric GUM_SCALAR >
451 DoCalculus< GUM_SCALAR >::doCalculus(const NodeSet& on, const NodeSet& doing) const {
452 return identifyingIntervention(on, doing);
453 }
454
455 template < GUM_Numeric GUM_SCALAR >
458 return identifyingIntervention(on, doing);
459 }
460
461 template < GUM_Numeric GUM_SCALAR >
464 const NodeSet& doing,
465 const NodeSet& knowing) const {
466 // IDC reduction: try to promote any Z∈knowing to doing if Z ⟂ on | doing ∪ (knowing\{Z})
467 // in the graph where incoming into doing and outgoing from knowing are removed.
468 if (!knowing.empty()) {
469 DAG gmod = _removeInIntoDoing_outOfKnowing_(_cm.causalDAG(), doing, knowing);
470
471 for (auto z: knowing) {
472 NodeSet Zi;
473 Zi.insert(z);
474 NodeSet Kminus = knowing - Zi;
475 if (Separation::isDSeparated(gmod, Zi, on, doing + Kminus)) {
476 // recurse with z moved to doing
477 NodeSet doing2 = doing + Zi;
478 NodeSet knowing2 = Kminus;
479 return doCalculusWithObservation(on, doing2, knowing2);
480 }
481 }
482 }
483
484 // Fallback: ratio P(on ∪ knowing | do(doing)) / P(knowing | do(doing))
485 NameList Yw, W;
486 for (auto y: on)
487 Yw.push_back(_cm.nameFromId(y));
488 for (auto w: knowing)
489 W.push_back(_cm.nameFromId(w));
490
491 NameList Xn;
492 for (auto x: doing)
493 Xn.push_back(_cm.nameFromId(x));
494
495 // numerator: ID on Y ∪ W ; denominator: ID on W (if W empty, denominator is 1)
496 NodeSet onUkn = on + knowing;
497
498 auto num = identifyingIntervention(onUkn, doing);
499 if (knowing.empty()) {
500 // P(Y|do(X)) when knowing is empty: just return numerator
501 return num;
502 }
503 auto den = identifyingIntervention(knowing, doing);
504 return std::make_unique< ASTdiv< GUM_SCALAR > >(std::move(num), std::move(den));
505 }
506
507 template < GUM_Numeric GUM_SCALAR >
510 const NameList& doing,
511 const NameList& knowing) const {
512 NodeSet ON, DO, KN;
513 for (const auto& s: on)
514 ON.insert(_cm.idFromName(s));
515 for (const auto& s: doing)
516 DO.insert(_cm.idFromName(s));
517 for (const auto& s: knowing)
518 KN.insert(_cm.idFromName(s));
519 return doCalculusWithObservation(ON, DO, KN);
520 }
521
522 /* ========================================================================== */
523 /* ID core */
524 /* ========================================================================== */
525
526 template < GUM_Numeric GUM_SCALAR >
529 const NodeSet& Y,
530 const NodeSet& X,
531 std::unique_ptr< ASTtree< GUM_SCALAR > > P) const {
532 const NodeSet lat = cm.latentVariablesIds();
533 const NodeSet V = cm.observationalBN().internalDag().nodes().asNodeSet();
534
535 // --- 1) if X is empty ---
536 if (X.empty()) {
537 if (!P) {
538 // return P(Y)
540 for (auto y: Y)
541 Yn.insert(cm.nameFromId(y));
542 return std::make_unique< ASTjointProba< GUM_SCALAR > >(Yn);
543 }
544 // return sum_{V\Y} P
545 NodeSet Vy = V - Y;
546 if (Vy.empty()) return std::move(P);
547 std::vector< std::string > tosum;
548 tosum.reserve(Vy.size());
549 for (auto v: Vy)
550 tosum.push_back(cm.nameFromId(v));
551 return std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(P));
552 }
553
554 // --- 2) restrict to ancestors of Y in G ---
555 {
556 DAG g = cm.causalDAG();
557 NodeSet A = _ancestorsIn_(g, Y);
558 A += Y;
559 // Only observed ancestors (V is observed-only)
560 NodeSet Aobs = A - lat;
561 if (Aobs.size() != V.size()) {
562 // sum out V \ Aobs from P if present, recurse on induced submodel G[Aobs]
563 if (P) {
564 NodeSet VminusA = V - Aobs;
565 if (!VminusA.empty()) {
566 std::vector< std::string > tosum;
567 tosum.reserve(VminusA.size());
568 for (auto v: VminusA)
569 tosum.push_back(cm.nameFromId(v));
570 P = std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(P));
571 }
572 }
573 auto sub = cm.inducedCausalSubModel(cm, Aobs);
574 NodeSet XA = X * Aobs;
575 return _ID_(sub, Y, XA, std::move(P));
576 }
577 }
578
579
580 // --- 3) W := (V\X) \ An_{G_{\overline X}}(Y); if non-empty, move W to X and recurse ---
581 {
582 DAG gXbar = _removeIncomingInto_(cm.causalDAG(), X);
583 NodeSet AnY = _ancestorsIn_(gXbar, Y);
584 AnY += Y;
585 NodeSet W = (V - X) - AnY;
586 if (!W.empty()) { return _ID_(cm, Y, X + W, std::move(P)); }
587 }
588
589 // --- 4) c-decompose G[V\X]; if >1 component, multiply and sum ---
590 {
591 auto gvx = cm.inducedCausalSubModel(cm, V - X);
592 auto icomp = _cDecompositionOn_(gvx);
593
594 if (icomp.size() > 1) {
595 // t = Π_i ID(S_i, V \ S_i)
596 std::unique_ptr< ASTtree< GUM_SCALAR > > t;
597 for (size_t i = 0; i < icomp.size(); ++i) {
598 NodeSet Si = icomp[i];
599 NameList Sn;
600 for (auto s: Si)
601 Sn.push_back(cm.nameFromId(s));
602
603 std::unique_ptr< ASTtree< GUM_SCALAR > > Pcopy
604 = P ? std::unique_ptr< ASTtree< GUM_SCALAR > >(P->copy()) : nullptr;
605 auto term = _ID_(cm, Si, V - Si, std::move(Pcopy));
606 if (!t) t = std::move(term);
607 else t = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(term), std::move(t));
608 }
609 NodeSet Vyx = V - (X + Y);
610 if (Vyx.empty()) return t;
611 std::vector< std::string > tosum;
612 tosum.reserve(Vyx.size());
613 for (auto v: Vyx)
614 tosum.push_back(cm.nameFromId(v));
615 return std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(t));
616 }
617
618 // Only one component:
619 NodeSet S = icomp.front();
620 auto cdg = _cDecomposition_(cm);
621
622 // --- 5) Hedge check: if full graph is one c-component, not identifiable
623 if (cdg.size() == 1) {
624 NodeSet all = cdg.front();
625 if (all.size() == V.size()) {
626 // build names for message
627 Set< std::string > Vn, Sn;
628 for (auto v: V)
629 Vn.insert(cm.nameFromId(v));
630 for (auto s: S)
631 Sn.insert(cm.nameFromId(s));
632 auto msg = _hedgeExceptionMsg(cm, Y, X, V, S);
634 }
635 }
636
637 // --- 6) If S is also a c-component in G: product of local conditionals over S
638 {
639 // test membership: S ∈ cdg ?
640 bool sInCdg = false;
641 for (const auto& comp: cdg) {
642 if (comp == S) {
643 sInCdg = true;
644 break;
645 }
646 }
647
648 if (sInCdg) {
649 // P_new := Π_{v∈S in topo order} P(v | v_π) under current P (if provided),
650 // otherwise P from the BN (chain rule)
651 const auto& bn = cm.observationalBN();
652 auto order = _topoObserved_(cm);
653
654 std::unique_ptr< ASTtree< GUM_SCALAR > > prod;
655
656 // Make a name lookup for quick topo prefix
657 std::unordered_map< NodeId, Size > pos;
658 for (Size i = 0; i < order.size(); ++i)
659 pos[order[i]] = i;
660
661 // build in topo order filtered to S
662 std::vector< NodeId > ordering;
663 ordering.reserve(S.size());
664 for (auto v: order)
665 if (S.contains(v)) ordering.push_back(v);
666
667 for (Size j = 0; j < ordering.size(); ++j) {
668 auto v = ordering[j];
669 std::unique_ptr< ASTtree< GUM_SCALAR > > term;
670 if (!P) {
671 // from BN: P(v | predecessors) using chain rule prefix
672 Size k = pos[v];
673 Set< std::string > condNames;
674 for (Size i = 0; i < k; ++i)
675 condNames.insert(cm.nameFromId(order[i]));
677 lhs.insert(cm.nameFromId(v));
678 if (condNames.empty()) term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
679 else term = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(bn, lhs, condNames);
680 } else {
681 // from accumulated P: factorize Q[S] by full-order prefix marginals
682 Size k = pos[v];
683 std::vector< std::string > prefixNames;
684 prefixNames.reserve(k + 1);
685 for (Size i = 0; i <= k; ++i)
686 prefixNames.push_back(cm.nameFromId(order[i]));
687
688 // numerator = marginal of P on current prefix
689 std::unique_ptr< ASTtree< GUM_SCALAR > > num(P->copy());
690 std::vector< std::string > sumNum;
691 for (auto u: V) {
692 const auto& un = cm.nameFromId(u);
693 if (std::find(prefixNames.begin(), prefixNames.end(), un) == prefixNames.end()) {
694 sumNum.push_back(un);
695 }
696 }
697 if (!sumNum.empty()) {
698 num = std::make_unique< ASTsum< GUM_SCALAR > >(sumNum, std::move(num));
699 }
700
701 if (k == 0) {
703 lhs.insert(cm.nameFromId(v));
704 term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
705
706 } else {
707 // denominator = marginal of P on previous prefix
708 std::unique_ptr< ASTtree< GUM_SCALAR > > den(P->copy());
709 std::vector< std::string > prevPrefix;
710 prevPrefix.reserve(k);
711 for (Size i = 0; i < k; ++i)
712 prevPrefix.push_back(cm.nameFromId(order[i]));
713 std::vector< std::string > sumDen;
714 for (auto u: V) {
715 const auto& un = cm.nameFromId(u);
716 if (std::find(prevPrefix.begin(), prevPrefix.end(), un) == prevPrefix.end()) {
717 sumDen.push_back(un);
718 }
719 }
720 if (!sumDen.empty()) {
721 den = std::make_unique< ASTsum< GUM_SCALAR > >(sumDen, std::move(den));
722 }
723 term = std::make_unique< ASTdiv< GUM_SCALAR > >(std::move(num), std::move(den));
724 }
725 }
726
727 if (!prod) prod = std::move(term);
728 else prod = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(term), std::move(prod));
729 }
730
731 NodeSet Sy = S - Y;
732 if (Sy.empty()) return prod;
733 std::vector< std::string > tosum;
734 tosum.reserve(Sy.size());
735 for (auto v: Sy)
736 tosum.push_back(cm.nameFromId(v));
737 return std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(prod));
738 }
739 }
740
741 // --- 7) Else: pick a c-component S' of G, build Q[S'] then recurse on G[S']
742 {
743 // choose the c-component of G that contains S (SP Step 7: S ⊆ S')
744 NodeSet Spr;
745 bool found = false;
746 for (const auto& comp: cdg) {
747 if ((S - comp).empty()) {
748 Spr = comp;
749 found = true;
750 break;
751 }
752 }
753 if (!found) {
754 // Fallback should not happen; be conservative
755 Spr = S;
756 }
757
758 const auto& bn = cm.observationalBN();
759 auto order = _topoObserved_(cm);
760
761 std::unique_ptr< ASTtree< GUM_SCALAR > > prod;
762
763 std::unordered_map< NodeId, Size > pos;
764 for (Size i = 0; i < order.size(); ++i)
765 pos[order[i]] = i;
766
767 std::vector< NodeId > ordering;
768 ordering.reserve(Spr.size());
769 for (auto v: order)
770 if (Spr.contains(v)) ordering.push_back(v);
771
772 for (Size j = 0; j < ordering.size(); ++j) {
773 auto v = ordering[j];
774 std::unique_ptr< ASTtree< GUM_SCALAR > > term;
775 if (!P) {
776 Size k = pos[v];
777 Set< std::string > condNames;
778 for (Size i = 0; i < k; ++i)
779 condNames.insert(cm.nameFromId(order[i]));
781 lhs.insert(cm.nameFromId(v));
782 if (condNames.empty()) term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
783 else term = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(bn, lhs, condNames);
784 } else {
785 // from accumulated P: factorize Q[Spr] by full-order prefix marginals
786 Size k = pos[v];
787 std::vector< std::string > prefixNames;
788 prefixNames.reserve(k + 1);
789 for (Size i = 0; i <= k; ++i)
790 prefixNames.push_back(cm.nameFromId(order[i]));
791
792 // numerator = marginal of P on current prefix
793 std::unique_ptr< ASTtree< GUM_SCALAR > > num(P->copy());
794 std::vector< std::string > sumNum;
795 for (auto u: V) {
796 const auto& un = cm.nameFromId(u);
797 if (std::find(prefixNames.begin(), prefixNames.end(), un) == prefixNames.end()) {
798 sumNum.push_back(un);
799 }
800 }
801 if (!sumNum.empty()) {
802 num = std::make_unique< ASTsum< GUM_SCALAR > >(sumNum, std::move(num));
803 }
804
805 if (k == 0) {
807 lhs.insert(cm.nameFromId(v));
808 term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
809
810 } else {
811 // denominator = marginal of P on previous prefix
812 std::unique_ptr< ASTtree< GUM_SCALAR > > den(P->copy());
813 std::vector< std::string > prevPrefix;
814 prevPrefix.reserve(k);
815 for (Size i = 0; i < k; ++i)
816 prevPrefix.push_back(cm.nameFromId(order[i]));
817 std::vector< std::string > sumDen;
818 for (auto u: V) {
819 const auto& un = cm.nameFromId(u);
820 if (std::find(prevPrefix.begin(), prevPrefix.end(), un) == prevPrefix.end()) {
821 sumDen.push_back(un);
822 }
823 }
824 if (!sumDen.empty()) {
825 den = std::make_unique< ASTsum< GUM_SCALAR > >(sumDen, std::move(den));
826 }
827 term = std::make_unique< ASTdiv< GUM_SCALAR > >(std::move(num), std::move(den));
828 }
829 }
830
831 if (!prod) prod = std::move(term);
832 else prod = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(term), std::move(prod));
833 }
834
835 auto sub = cm.inducedCausalSubModel(cm, Spr);
836 NodeSet Yspr = Y * Spr; // <— important
837 NodeSet Xspr = X * Spr;
838
839 if (Yspr.empty()) {
841 "Internal error in ID step 7: chosen C-component is disjoint from Y.");
842 }
843
844 return _ID_(sub, Yspr, Xspr, std::move(prod));
845 }
846 }
847 }
848
849} // namespace gum
d-separation utilities
Represents an identified causal formula and its query context.
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.
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.
CausalModel< GUM_SCALAR > inducedCausalSubModel(const CausalModel< GUM_SCALAR > &cm, NodeSet subset) const
Induced causal submodel on a subset of nodes.
NodeSet latentVariablesIds() const
Node ids of all latent variables.
std::string nameFromId(NodeId id) const
Variable name from node id (observed or latent).
NodeSet children(NodeId x) const
Children of a node (by id) in the causal DAG (including latents).
const BayesNet< GUM_SCALAR > & observationalBN() const
Observational BN (observed variables only).
Base class for dag.
Definition DAG.h:121
static DAG _removeInIntoDoing_outOfKnowing_(const DAG &dag, const NodeSet &doing, const NodeSet &knowing)
IDC helper: DAG copy with incoming into doing removed AND outgoing from knowing removed.
FormulaPtr doCalculusWithObservation(const NodeSet &on, const NodeSet &doing, const NodeSet &knowing) const
IDC: identify P(on | do(doing), knowing).
FormulaPtr _ID_(const CausalModel< GUM_SCALAR > &cm, const NodeSet &Y, const NodeSet &X, std::unique_ptr< ASTtree< GUM_SCALAR > > P) const
ID core (recursive), with optional accumulated distribution P (AST) to carry in decompositions.
static DAG _removeIncomingInto_(const DAG &dag, const NodeSet &X)
DAG copy with all incoming arcs into X removed (G_{\overline X}).
const CausalModel< GUM_SCALAR > & _cm
Definition doCalculus.h:127
std::vector< NodeId > _topoObserved_(const CausalModel< GUM_SCALAR > &cm) const
Topological order over observed nodes (ignoring latent parents).
std::vector< std::string > NameList
Convenience container for name-based overloads.
Definition doCalculus.h:84
FormulaPtr getFrontDoorTree(NodeId cause, NodeId effect, const NodeSet &zset) const
std::vector< NodeSet > _cDecompositionOn_(const CausalModel< GUM_SCALAR > &sub) const
c-decomposition on an induced submodel (observed nodes only).
std::unique_ptr< ASTtree< GUM_SCALAR > > FormulaPtr
Owned pointer to an AST representing a probability expression.
Definition doCalculus.h:81
static NodeSet _ancestorsIn_(const DAG &g, const NodeSet &T)
Ancestors (in DAG g) of a set T (including T).
FormulaPtr doCalculus(const NodeSet &on, const NodeSet &doing) const
Thin wrapper around ID: doCalculus(on, doing).
std::vector< NodeSet > _cDecomposition_(const CausalModel< GUM_SCALAR > &cm) const
c-decomposition (confounding components) among observed nodes.
static std::string _hedgeExceptionMsg(const CausalModel< GUM_SCALAR > &cm, const NodeSet &Y, const NodeSet &X, const NodeSet &V, const NodeSet &S)
Format a hedge exception message.
FormulaPtr identifyingIntervention(const NodeSet &Y, const NodeSet &X) const
ID: identify P(Y | do(X)) (NodeId-based).
FormulaPtr getBackDoorTree(NodeId cause, NodeId effect, const NodeSet &zset) const
const CausalModel< GUM_SCALAR > & model() const noexcept
Exception : "hedge" (witness of non-identifiability) is detected in do-calculus / ID computations.
Exception: at least one argument passed to a function is not what was expected.
const NodeGraphPart & nodes() const
return *this as a NodeGraphPart
NodeSet asNodeSet() const
returns a copy of the set of nodes represented by the NodeGraphPart
Exception : operation not allowed.
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).
bool contains(const Key &k) const
Indicates whether a given elements belong to the set.
Definition set_tpl.h:468
bool empty() const noexcept
Indicates whether the set is the empty set.
Definition set_tpl.h:613
void insert(const Key &k)
Inserts a new element into the set.
Definition set_tpl.h:510
Size size() const noexcept
Returns the number of elements in the set.
Definition set_tpl.h:607
Do-calculus utilities and AST builders bound to a single CausalModel.
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Definition types.h:74
Size NodeId
Type for node ids.
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
std::unique_ptr< ASTtree< GUM_SCALAR > > productOfTrees(std::vector< std::unique_ptr< ASTtree< GUM_SCALAR > > > &&lterms)
Build a product AST from a list of terms.
Definition doAST_tpl.h:633