60#include <unordered_map>
61#include <unordered_set>
68 inline std::string _fmtBraceList_(std::vector< std::string > v) {
69 std::sort(v.begin(), v.end());
71 for (
size_t i = 0; i < v.size(); ++i) {
81 template < GUM_Numeric GUM_SCALAR >
90 template < GUM_Numeric GUM_SCALAR >
99 "getBackDoorTree: 'cause' and 'effect' must be observed (non-latent).");
108 const auto& dag =
_cm.causalDAG();
109 auto mapO =
_cm.id2name(
false);
110 const std::string xName =
_cm.nameFromId(cause);
111 const std::string yName =
_cm.nameFromId(effect);
114 std::vector< std::string > zOrder;
115 zOrder.reserve(zset.
size());
117 const std::string zn =
_cm.nameFromId(z);
119 zOrder.push_back(zn);
121 std::sort(zOrder.begin(), zOrder.end());
128 for (
const auto& zn: zNames)
130 auto Pyxz = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(dag, mapO, lhsY, knw);
132 if (zNames.
empty()) {
138 auto Pz = std::make_unique< ASTjointProba< GUM_SCALAR > >(zNames);
141 std::vector< std::unique_ptr< ASTtree< GUM_SCALAR > > > terms;
142 terms.emplace_back(std::move(Pyxz));
143 terms.emplace_back(std::move(Pz));
146 return std::make_unique< ASTsum< GUM_SCALAR > >(zOrder, std::move(prod));
149 template < GUM_Numeric GUM_SCALAR >
152 std::string_view effectName,
154 const NodeId x =
_cm.idFromName(causeName);
155 const NodeId y =
_cm.idFromName(effectName);
158 for (
const auto& zn: zNames)
164 template < GUM_Numeric GUM_SCALAR >
173 "getFrontDoorTree: 'cause' and 'effect' must be observed (non-latent).");
188 const std::string xName =
_cm.nameFromId(cause);
189 const std::string yName =
_cm.nameFromId(effect);
192 std::vector< std::string > zOrder;
193 zOrder.reserve(zset.
size());
195 const auto zn =
_cm.nameFromId(z);
197 zOrder.push_back(zn);
199 std::sort(zOrder.begin(), zOrder.end());
202 const auto& dag =
_cm.causalDAG();
203 auto mapO =
_cm.id2name(
false);
207 auto Pz_given_x = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(dag, mapO, zNames, condX);
215 auto Py_given_xz = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(dag, mapO, ySet, condXZ);
219 auto Px = std::make_unique< ASTjointProba< GUM_SCALAR > >(xSet);
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));
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));
232 template < GUM_Numeric GUM_SCALAR >
235 std::string_view effectName,
237 const NodeId x =
_cm.idFromName(causeName);
238 const NodeId y =
_cm.idFromName(effectName);
241 for (
const auto& zn: zNames)
251 template < GUM_Numeric GUM_SCALAR >
258 auto namesOf = [&](
const NodeSet& Ns) {
259 std::vector< std::string > out;
260 out.reserve(Ns.size());
263 std::sort(out.begin(), out.end());
267 const auto yN = namesOf(Y);
268 const auto xN = namesOf(X);
269 const auto vN = namesOf(V);
270 const auto sN = namesOf(S);
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.",
284 template < GUM_Numeric GUM_SCALAR >
285 std::vector< NodeSet >
292 std::unordered_map< NodeId, std::vector< NodeId > > adj;
299 for (
auto it1 = ch.begin(); it1 != ch.end(); ++it1) {
303 for (; it2 != ch.end(); ++it2) {
305 adj[*it1].push_back(*it2);
306 adj[*it2].push_back(*it1);
312 std::vector< NodeSet > comps;
313 std::unordered_set< NodeId > seen;
315 if (seen.count(v))
continue;
317 std::queue< NodeId > q;
324 for (
auto w: adj[u]) {
325 if (!seen.count(w)) {
332 comps.push_back(std::move(comp));
337 template < GUM_Numeric GUM_SCALAR >
338 std::vector< NodeSet >
343 template < GUM_Numeric GUM_SCALAR >
344 std::vector< NodeId >
347 const NodeSet V = dag.nodes().asNodeSet();
350 std::unordered_map< NodeId, Size > indeg;
352 indeg[v] = dag.parents(v).size();
354 std::vector< NodeId > order;
355 order.reserve(V.
size());
358 std::queue< NodeId > q;
359 for (
const auto& kv: indeg)
360 if (kv.second == 0) q.push(kv.first);
363 const NodeId u = q.front();
367 for (
auto c: dag.children(u)) {
370 if (indeg[c] == 0) q.push(c);
377 template < GUM_Numeric GUM_SCALAR >
381 std::queue< NodeId > q;
398 template < GUM_Numeric GUM_SCALAR >
409 template < GUM_Numeric GUM_SCALAR >
414 for (
auto x: doing) {
419 for (
auto z: knowing) {
431 template < GUM_Numeric GUM_SCALAR >
434 return _ID_(
_cm, Y, X,
nullptr);
437 template < GUM_Numeric GUM_SCALAR >
442 for (
const auto& s: Y)
444 for (
const auto& s: X)
449 template < GUM_Numeric GUM_SCALAR >
455 template < GUM_Numeric GUM_SCALAR >
461 template < GUM_Numeric GUM_SCALAR >
465 const NodeSet& knowing)
const {
468 if (!knowing.
empty()) {
471 for (
auto z: knowing) {
487 Yw.push_back(
_cm.nameFromId(y));
488 for (
auto w: knowing)
489 W.push_back(
_cm.nameFromId(w));
493 Xn.push_back(
_cm.nameFromId(x));
499 if (knowing.
empty()) {
504 return std::make_unique< ASTdiv< GUM_SCALAR > >(std::move(num), std::move(den));
507 template < GUM_Numeric GUM_SCALAR >
513 for (
const auto& s: on)
515 for (
const auto& s: doing)
517 for (
const auto& s: knowing)
526 template < GUM_Numeric GUM_SCALAR >
531 std::unique_ptr< ASTtree< GUM_SCALAR > > P)
const {
542 return std::make_unique< ASTjointProba< GUM_SCALAR > >(Yn);
546 if (Vy.
empty())
return std::move(P);
547 std::vector< std::string > tosum;
548 tosum.reserve(Vy.
size());
551 return std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(P));
565 if (!VminusA.
empty()) {
566 std::vector< std::string > tosum;
567 tosum.reserve(VminusA.
size());
568 for (
auto v: VminusA)
570 P = std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(P));
575 return _ID_(sub, Y, XA, std::move(P));
586 if (!W.
empty()) {
return _ID_(cm, Y, X + W, std::move(P)); }
594 if (icomp.size() > 1) {
596 std::unique_ptr< ASTtree< GUM_SCALAR > > t;
597 for (
size_t i = 0; i < icomp.size(); ++i) {
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));
610 if (Vyx.
empty())
return t;
611 std::vector< std::string > tosum;
612 tosum.reserve(Vyx.
size());
615 return std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(t));
623 if (cdg.size() == 1) {
641 for (
const auto& comp: cdg) {
654 std::unique_ptr< ASTtree< GUM_SCALAR > > prod;
657 std::unordered_map< NodeId, Size > pos;
658 for (
Size i = 0; i < order.size(); ++i)
662 std::vector< NodeId > ordering;
663 ordering.reserve(S.
size());
665 if (S.
contains(v)) ordering.push_back(v);
667 for (
Size j = 0; j < ordering.size(); ++j) {
668 auto v = ordering[j];
669 std::unique_ptr< ASTtree< GUM_SCALAR > > term;
674 for (
Size i = 0; i < k; ++i)
678 if (condNames.
empty()) term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
679 else term = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(bn, lhs, condNames);
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]));
689 std::unique_ptr< ASTtree< GUM_SCALAR > > num(P->copy());
690 std::vector< std::string > sumNum;
693 if (std::find(prefixNames.begin(), prefixNames.end(), un) == prefixNames.end()) {
694 sumNum.push_back(un);
697 if (!sumNum.empty()) {
698 num = std::make_unique< ASTsum< GUM_SCALAR > >(sumNum, std::move(num));
704 term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
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;
716 if (std::find(prevPrefix.begin(), prevPrefix.end(), un) == prevPrefix.end()) {
717 sumDen.push_back(un);
720 if (!sumDen.empty()) {
721 den = std::make_unique< ASTsum< GUM_SCALAR > >(sumDen, std::move(den));
723 term = std::make_unique< ASTdiv< GUM_SCALAR > >(std::move(num), std::move(den));
727 if (!prod) prod = std::move(term);
728 else prod = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(term), std::move(prod));
732 if (Sy.
empty())
return prod;
733 std::vector< std::string > tosum;
734 tosum.reserve(Sy.
size());
737 return std::make_unique< ASTsum< GUM_SCALAR > >(tosum, std::move(prod));
746 for (
const auto& comp: cdg) {
747 if ((S - comp).empty()) {
761 std::unique_ptr< ASTtree< GUM_SCALAR > > prod;
763 std::unordered_map< NodeId, Size > pos;
764 for (
Size i = 0; i < order.size(); ++i)
767 std::vector< NodeId > ordering;
768 ordering.reserve(Spr.
size());
770 if (Spr.
contains(v)) ordering.push_back(v);
772 for (
Size j = 0; j < ordering.size(); ++j) {
773 auto v = ordering[j];
774 std::unique_ptr< ASTtree< GUM_SCALAR > > term;
778 for (
Size i = 0; i < k; ++i)
782 if (condNames.
empty()) term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
783 else term = std::make_unique< ASTposteriorProba< GUM_SCALAR > >(bn, lhs, condNames);
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]));
793 std::unique_ptr< ASTtree< GUM_SCALAR > > num(P->copy());
794 std::vector< std::string > sumNum;
797 if (std::find(prefixNames.begin(), prefixNames.end(), un) == prefixNames.end()) {
798 sumNum.push_back(un);
801 if (!sumNum.empty()) {
802 num = std::make_unique< ASTsum< GUM_SCALAR > >(sumNum, std::move(num));
808 term = std::make_unique< ASTjointProba< GUM_SCALAR > >(lhs);
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;
820 if (std::find(prevPrefix.begin(), prevPrefix.end(), un) == prevPrefix.end()) {
821 sumDen.push_back(un);
824 if (!sumDen.empty()) {
825 den = std::make_unique< ASTsum< GUM_SCALAR > >(sumDen, std::move(den));
827 term = std::make_unique< ASTdiv< GUM_SCALAR > >(std::move(num), std::move(den));
831 if (!prod) prod = std::move(term);
832 else prod = std::make_unique< ASTmult< GUM_SCALAR > >(std::move(term), std::move(prod));
841 "Internal error in ID step 7: chosen C-component is disjoint from Y.");
844 return _ID_(sub, Yspr, Xspr, std::move(prod));
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.
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).
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
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.
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.
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.
bool empty() const noexcept
Indicates whether the set is the empty set.
void insert(const Key &k)
Inserts a new element into the set.
Size size() const noexcept
Returns the number of elements in the set.
Do-calculus utilities and AST builders bound to a single CausalModel.
#define GUM_ERROR(type, msg)
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Size NodeId
Type for node ids.
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
gum is the global namespace for all aGrUM entities
std::unique_ptr< ASTtree< GUM_SCALAR > > productOfTrees(std::vector< std::unique_ptr< ASTtree< GUM_SCALAR > > > &<erms)
Build a product AST from a list of terms.