aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
causalModel_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
46
47#pragma once
48#include <optional>
49#include <sstream>
50
53
54namespace gum {
55
56 // ===============================
57 // Constructors / destructor
58 // ===============================
59
60 template < GUM_Numeric GUM_SCALAR >
63 GUM_CONSTRUCTOR(CausalModel)
64 }
65
66 template < GUM_Numeric GUM_SCALAR >
70
71 template < GUM_Numeric GUM_SCALAR >
73 const LatentDescriptorVector& latentVarsDescriptor,
74 bool assumeNonSpurious) :
76 // add each latent (children given as names)
77 for (const auto& [latent, children]: latentVarsDescriptor) {
79 }
80 GUM_CONSTRUCTOR(CausalModel)
81 }
82
84 template < GUM_Numeric GUM_SCALAR >
90
92 template < GUM_Numeric GUM_SCALAR >
94 _observationalBN_(std::move(other._observationalBN_)),
95 _causalDAG_(std::move(other._causalDAG_)), _ids_names_(std::move(other._ids_names_)) {
96 GUM_CONS_MOV(CausalModel)
97 };
98
99 // ===============================
100 // Latent variables
101 // ===============================
102
103 template < GUM_Numeric GUM_SCALAR >
105 std::string_view latentName,
106 const std::vector< std::string >& childrenOfLatent,
107 bool assumeNonSpurious) {
108 std::vector< NodeId > childIds;
109 childIds.reserve(childrenOfLatent.size());
110 for (const auto& nm: childrenOfLatent) {
111 childIds.push_back(idFromName(nm));
112 }
113 addLatentVariable(latentName, childIds, assumeNonSpurious);
114 }
115
116 template < GUM_Numeric GUM_SCALAR >
117 void CausalModel< GUM_SCALAR >::addLatentVariable(std::string_view latentName,
118 const std::vector< NodeId >& childrenOfLatent,
119 bool assumeNonSpurious) {
120 // must have at least 2 children
121 if (childrenOfLatent.size() < 2) {
122 GUM_ERROR(InvalidArgument, "A latent variable must affect at least 2 children.");
123 }
124
125 // reject names that collide with observed or existing latent variables
126 if (_observationalBN_.variableNodeMap().exists(latentName)) {
128 "Latent variable name conflicts with an observed variable: "
129 + std::string(latentName));
130 }
131 if (_ids_names_.existsSecond(std::string(latentName))) {
133 "Latent variable name already used by another latent: " + std::string(latentName));
134 }
135
136 // create a fresh node id for the latent in the causal DAG
137 const NodeId id_latent = _causalDAG_.addNode();
138
139 // record bookkeeping
140 _ids_names_.insert(id_latent, std::string(latentName));
141
142 // add arcs latent -> each child
143 for (const auto& cid: childrenOfLatent) {
144 // children are observed nodes: they must already exist in causal DAG (copied from observed
145 // BN)
146 _causalDAG_.addArc(id_latent, cid);
147 }
148
149 // remove any direct causal arc among children unless assumeNonSpurious=true
150 if (!assumeNonSpurious) {
151 for (auto i = childrenOfLatent.begin(); i != childrenOfLatent.end(); ++i) {
152 auto j = i;
153 ++j;
154 for (auto jj = j; jj != childrenOfLatent.end(); ++jj) {
155 const NodeId a = *i;
156 const NodeId b = *jj;
157 // remove either direction if present
158 Arc ab(a, b), ba(b, a);
159 if (_causalDAG_.existsArc(ab)) _causalDAG_.eraseArc(ab);
160 else if (_causalDAG_.existsArc(ba)) _causalDAG_.eraseArc(ba);
161 }
162 }
163 }
164 }
165
166 template < GUM_Numeric GUM_SCALAR >
168 return _causalDAG_.existsArc(Arc(x, y));
169 }
170
171 template < GUM_Numeric GUM_SCALAR >
172 bool CausalModel< GUM_SCALAR >::existsArc(std::string_view x, std::string_view y) const {
173 const NodeId ix = idFromName(x);
174 const NodeId iy = idFromName(y);
175 return existsArc(ix, iy);
176 }
177
178 // ===============================
179 // Latent getters
180 // ===============================
181
182 template < GUM_Numeric GUM_SCALAR >
184 NodeSet latentIds;
185 for (auto it = _ids_names_.begin(); it != _ids_names_.end(); ++it) {
186 latentIds.insert(it.first());
188 return latentIds;
189 }
190
191 template < GUM_Numeric GUM_SCALAR >
193 Set< std::string > latentNames;
194 for (auto it = _ids_names_.begin(); it != _ids_names_.end(); ++it) {
195 latentNames.insert(it.second());
196 }
197 return latentNames;
198 }
199
200 // ===============================
201 // Spurious arc management
202 // ===============================
203
204 template < GUM_Numeric GUM_SCALAR >
206 // Validate arc in observationalBN
207 if (!_observationalBN_.internalDag().existsArc(Arc(x, y))) {
209 "Arc(" + std::to_string(x) + "," + std::to_string(y)
210 + ") not present in observationalBN");
211 }
212 // Remove arc from causalDAG if present
213 if (_causalDAG_.existsArc(Arc(x, y))) { _causalDAG_.eraseArc(Arc(x, y)); }
214 }
215
216 template < GUM_Numeric GUM_SCALAR >
217 void CausalModel< GUM_SCALAR >::assumeSpurious(std::string_view x, std::string_view y) {
219 }
220
221 template < GUM_Numeric GUM_SCALAR >
223 // Validate arc in observationalBN
224 if (!_observationalBN_.internalDag().existsArc(Arc(x, y))) {
226 "Arc(" + std::to_string(x) + "," + std::to_string(y)
227 + ") not present in observationalBN");
228 }
229 // Add arc to causalDAG if absent
230 if (!_causalDAG_.existsArc(Arc(x, y))) { _causalDAG_.addArc(x, y); }
231 }
232
233 template < GUM_Numeric GUM_SCALAR >
234 void CausalModel< GUM_SCALAR >::assumeNonSpurious(std::string_view x, std::string_view y) {
236 }
237
238 template < GUM_Numeric GUM_SCALAR >
240 return _observationalBN_.internalDag().existsArc(Arc(x, y))
241 && !_causalDAG_.existsArc(Arc(x, y));
242 }
243
244 template < GUM_Numeric GUM_SCALAR >
245 bool CausalModel< GUM_SCALAR >::isAssumedSpurious(std::string_view x, std::string_view y) const {
247 }
248
249 // ===============================
250 // Connected components
251 // ===============================
252
253 // -----------------------------------------------------------------------------
254 // Connected components (weak connectivity) on the CAUSAL DAG.
255 // This adapts the inner pyAgrum implementation:
256 // - Work over _causalDAG_.nodes()
257 // - Traverse using parents() and children()
258 // - Keep a "remaining" set; pop an arbitrary root for each component
259 // -----------------------------------------------------------------------------
260 template < GUM_Numeric GUM_SCALAR >
263
264 // Build a mutable "remaining" set from the DAG's node view
265 NodeSet remaining;
266 for (auto n: _causalDAG_.nodes())
267 remaining.insert(n);
268
269 while (!remaining.empty()) {
270 const NodeId root = *remaining.begin();
271
273 std::vector< NodeId > stack{root};
274 remaining.erase(root);
275
276 while (!stack.empty()) {
277 const NodeId u = stack.back();
278 stack.pop_back();
279 cc.insert(u);
280
281 // weak neighbours = parents ∪ children
282 for (auto v: _causalDAG_.parents(u)) {
283 if (remaining.contains(v)) {
284 remaining.erase(v);
285 stack.push_back(v);
286 }
287 }
288 for (auto v: _causalDAG_.children(u)) {
289 if (remaining.contains(v)) {
290 remaining.erase(v);
291 stack.push_back(v);
292 }
294 }
295
296 comps.insert(root, std::move(cc));
297 }
298
299 return comps;
300 }
301
302 // causalModel_tpl.h
303 template < GUM_Numeric GUM_SCALAR >
306 NodeSet subset) const {
307 // If caller gave an empty subset, use "all observed" nodes
308 if (subset.empty()) {
309 for (auto n: cm.observationalBN().nodes())
310 subset.insert(n);
312
313 // Keep only OBSERVED nodes (drop latents explicitly)
314 NodeSet nodes = subset;
315 for (auto L: cm.latentVariablesIds())
316 nodes.erase(L);
317
318 // --- Build an observational BN restricted to `nodes`
319 BayesNet< GUM_SCALAR > bn;
320 HashTable< NodeId, NodeId > idmap; // oldId -> newId
321
322 // add variables
323 for (auto n: nodes) {
324 const auto& var = cm.observationalBN().variable(n);
325 const NodeId nn = bn.add(var, n); // API accepts "add(var, id)"
326 idmap.insert(n, nn);
327 }
328
329 // add arcs among the kept observed nodes (from the *causal* DAG)
330 const auto causalDag = cm.causalDAG();
331 for (const auto& a: causalDag.arcs()) {
332 const NodeId x = a.tail();
333 const NodeId y = a.head();
334 if (nodes.contains(x) && nodes.contains(y)) { bn.addArc(idmap[x], idmap[y]); }
335 }
336
337 // --- Rebuild latent descriptor restricted to kept nodes
338 LatentDescriptorVector latentDesc;
339 for (auto L: cm.latentVariablesIds()) {
340 std::vector< NodeId > mapped_children;
341 mapped_children.reserve(4);
342
343 // intersect latent children with kept observed nodes
344 for (auto c: cm.children(L)) {
345 if (nodes.contains(c)) mapped_children.push_back(idmap[c]);
346 }
347
348 // keep the latent ONLY if it still has at least 2 children
349 if (mapped_children.size() >= 2) {
350 latentDesc.emplace_back(cm.nameFromId(L), std::move(mapped_children));
351 }
352 }
353
354
355 // assumeNonSpurious=true : pyagrum uses True here (do not remove arcs among children)
356 return CausalModel< GUM_SCALAR >(bn, latentDesc, /*assumeNonSpurious=*/true);
357 }
358
359 // ===============================
360 // Parents / Children (by id / by name)
361 // ===============================
362
363 template < GUM_Numeric GUM_SCALAR >
365 // delegate to the causal DAG (includes latent->observed arcs)
366 return _causalDAG_.parents(x);
367 }
368
369 template < GUM_Numeric GUM_SCALAR >
370 NodeSet CausalModel< GUM_SCALAR >::parents(std::string_view name) const {
371 return parents(idFromName(name));
372 }
373
374 template < GUM_Numeric GUM_SCALAR >
376 return _causalDAG_.children(x);
377 }
378
379 template < GUM_Numeric GUM_SCALAR >
380 NodeSet CausalModel< GUM_SCALAR >::children(std::string_view name) const {
381 return children(idFromName(name));
382 }
383
384 /* =======================================================================
385 * Backdoor / Frontdoor conveniences (IDs only)
386 * ======================================================================= */
387
388 template < GUM_Numeric GUM_SCALAR >
389 std::optional< NodeSet > CausalModel< GUM_SCALAR >::backDoor(NodeId cause, NodeId effect) const {
390 // Preconditions: cause/effect must be observed (non-latent).
391 const NodeSet lat = latentVariablesIds();
392 if (lat.contains(cause) || lat.contains(effect)) {
394 "CausalModel::backDoor: 'cause' and 'effect' must be observed (non-latent).");
395 }
396
397 const auto candidates = DoorCriteria::enumerateBackdoorSets(_causalDAG_, cause, effect, lat);
398 if (candidates.empty()) return std::nullopt;
399 return candidates.front(); // first valid set (may itself be empty)
400 }
401
402 template < GUM_Numeric GUM_SCALAR >
403 std::optional< NodeSet > CausalModel< GUM_SCALAR >::frontDoor(NodeId cause, NodeId effect) const {
404 // Preconditions: cause/effect must be observed (non-latent).
405 const NodeSet lat = latentVariablesIds();
406 if (lat.contains(cause) || lat.contains(effect)) {
408 "CausalModel::frontDoor: 'cause' and 'effect' must be observed (non-latent).");
409 }
410
411 const auto candidates = DoorCriteria::enumerateFrontdoorSets(_causalDAG_, cause, effect, lat);
412 if (candidates.empty()) return std::nullopt;
413 return candidates.front(); // first valid set (may itself be empty)
414 }
415
416 // ===============================
417 // DOT export
418 // ===============================
419 template < GUM_Numeric GUM_SCALAR >
420 std::string CausalModel< GUM_SCALAR >::toDot(const bool SHOW_LATENT_NAMES,
421 const char* NODE_BG,
422 const char* NODE_FG,
423 const char* EDGE_COL) const {
424 // SHOW_LATENT_NAMES => causal.show_latent_names
425 // NODE_BG => causal.default_node_bgcolor
426 // NODE_FG => causal.default_node_fgcolor
427 // EDGE_COL => notebook.default_arc_color
428
429 std::stringstream out;
430 out << "digraph {\n";
431
432 // First, latent nodes
433 for (const auto n: _causalDAG_.nodes()) {
434 if (latentVariablesIds().contains(n)) {
435 std::string label;
436 try {
437 label = nameFromId(n);
438 } catch (gum::NotFound&) { label = "L" + std::to_string(n); }
439 out << " \"" << label << "\" "
440 << "[fillcolor=\"" << NODE_BG << "\", "
441 << "fontcolor=\"" << NODE_FG << "\", "
442 << "style=filled,shape=" << (SHOW_LATENT_NAMES ? "ellipse" : "point") << "];\n";
443 }
444 }
445
446 // Then, observed nodes
447 for (const auto n: _causalDAG_.nodes()) {
448 if (!latentVariablesIds().contains(n)) {
449 std::string label;
450 try {
451 label = nameFromId(n);
452 } catch (gum::NotFound&) { label = "L" + std::to_string(n); }
453 out << " \"" << label << "\" "
454 << "[fillcolor=\"" << NODE_BG << "\", "
455 << "fontcolor=\"" << NODE_FG << "\", "
456 << "style=filled,shape=ellipse];\n";
457 }
458 }
459
460 // Edges
461 for (const auto tail: _causalDAG_.nodes()) {
462 for (const auto head: _causalDAG_.children(tail)) {
463 std::string lt, lh;
464 try {
465 lt = nameFromId(tail);
466 } catch (gum::NotFound&) { lt = "L" + std::to_string(tail); }
467 try {
468 lh = nameFromId(head);
469 } catch (gum::NotFound&) { lh = "L" + std::to_string(head); }
470
471 const bool dashed
472 = latentVariablesIds().contains(tail) || latentVariablesIds().contains(head);
473
474 out << " \"" << lt << "\"->\"" << lh << "\" ";
475 if (dashed) {
476 out << "[style=dashed]";
477 } else {
478 // match Python: color="#4A4A4A:#4A4A4A"
479 out << "[color=\"" << EDGE_COL << ":" << EDGE_COL << "\"]";
480 }
481 out << ";\n";
482 }
483 }
484
485 out << "}\n";
486 return out.str();
487 }
488
489 // ===============================
490 // Names and IDs Getters
491 // ===============================
492
493 template < GUM_Numeric GUM_SCALAR >
496 for (const auto n: _observationalBN_.nodes()) {
497 names.insert(_observationalBN_.variable(n).name());
498 }
499 // latent names (from bijection)
500 for (auto it = _ids_names_.begin(); it != _ids_names_.end(); ++it) {
501 names.insert(it.second());
502 }
503 return names;
504 }
505
506 template < GUM_Numeric GUM_SCALAR >
507 NodeId CausalModel< GUM_SCALAR >::idFromName(std::string_view name) const {
508 if (_ids_names_.existsSecond(std::string(name))) {
509 return _ids_names_.first(std::string(name));
510 } else {
511 return _observationalBN_.idFromName(name);
512 }
513 }
514
515 template < GUM_Numeric GUM_SCALAR >
517 if (_ids_names_.existsFirst(id)) {
518 return _ids_names_.second(id);
519 } else {
520 return _observationalBN_.variable(id).name();
521 }
522 }
523
524 template < GUM_Numeric GUM_SCALAR >
526 CausalModel< GUM_SCALAR >::id2name(bool includeLatentVariables) const {
528
529 // Add observed variables
530 for (const auto n: _observationalBN_.nodes()) {
531 result.insert(n, _observationalBN_.variable(n).name());
532 }
533
534 // Add latent variables if requested
535 if (includeLatentVariables) {
536 for (auto iter = _ids_names_.beginSafe(); iter != _ids_names_.endSafe(); ++iter) {
537 result.insert(iter.first(), iter.second());
538 }
539 }
540
541 return result;
542 }
543
544 // ===============================
545 // Inline accessors
546 // ===============================
547
548 template < GUM_Numeric GUM_SCALAR >
549 const BayesNet< GUM_SCALAR >& CausalModel< GUM_SCALAR >::observationalBN() const {
550 return _observationalBN_;
551 }
552
553 template < GUM_Numeric GUM_SCALAR >
555 DAG g = _causalDAG_;
556 for (auto id: g)
557 g.setName(id, nameFromId(id));
558 return g;
559 }
560
561 template < GUM_Numeric GUM_SCALAR >
562 std::optional< NodeSet > CausalModel< GUM_SCALAR >::backDoor(std::string_view cause,
563 std::string_view effect) const {
564 return backDoor(idFromName(cause), idFromName(effect));
565 }
566
567 template < GUM_Numeric GUM_SCALAR >
568 std::optional< NodeSet > CausalModel< GUM_SCALAR >::frontDoor(std::string_view cause,
569 std::string_view effect) const {
570 return frontDoor(idFromName(cause), idFromName(effect));
571 }
572
573 template < GUM_Numeric GUM_SCALAR >
575 return _observationalBN_.variable(id);
576 }
577
578 template < GUM_Numeric GUM_SCALAR >
579 const DiscreteVariable& CausalModel< GUM_SCALAR >::variable(std::string_view name) const {
580 return _observationalBN_.variable(idFromName(name));
581 }
582
583} // namespace gum
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.
std::string toDot(const bool SHOW_LATENT_NAMES=false, const char *NODE_BG="#404040", const char *NODE_FG="white", const char *EDGE_COL="#4A4A4A") const
DOT representation of the causal DAG (observed + latent).
void addLatentVariable(std::string_view latentName, const std::vector< std::string > &childrenOfLatent, bool assumeNonSpurious=false)
Add a latent variable by names of its observed children.
CausalModel< GUM_SCALAR > inducedCausalSubModel(const CausalModel< GUM_SCALAR > &cm, NodeSet subset) const
Induced causal submodel on a subset of nodes.
~CausalModel()
Destructor.
NodeSet latentVariablesIds() const
Node ids of all latent variables.
DAG _causalDAG_
The underlying DAG representing the causal structure (observed + latent).
Definition causalModel.h:90
HashTable< NodeId, NodeSet > connectedComponents() const
Weakly connected components of the causal DAG.
bool isAssumedSpurious(NodeId x, NodeId y) const
Returns true if arc is present in observationalBN and absent in causalDAG.
Set< std::string > latentVariablesNames() const
Names of all latent variables.
std::optional< NodeSet > frontDoor(NodeId cause, NodeId effect) const
Find a frontdoor adjustment set (Z) between cause and effect (IDs only).
Bijection< NodeId, std::string > id2name(bool includeLatentVariable=false) const
Bidirectional mapping between node ids and variable names.
Bijection< NodeId, std::string > _ids_names_
Bidirectional mapping between node ids and variable names (observed + latent).
Definition causalModel.h:93
NodeId idFromName(std::string_view name) const
Node id from variable name (observed or latent).
bool existsArc(NodeId x, NodeId y) const
Whether a causal arc x → y exists (by ids) in the causal DAG.
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 DiscreteVariable & variable(NodeId id) const
From id to variable (observed only, by id).
BayesNet< GUM_SCALAR > _observationalBN_
The underlying BayesNet representing the observed part of the model.
Definition causalModel.h:87
CausalModel()=delete
Default constructor disabled: a causal model must be built from a BN.
Set< std::string > names() const
All variable names appearing in the causal model (observed + latent).
void assumeNonSpurious(NodeId x, NodeId y)
Mark an arc as non-spurious: present in observationalBN, added to causalDAG.
std::optional< NodeSet > backDoor(NodeId cause, NodeId effect) const
Find a backdoor adjustment set (Z) between cause and effect (IDs only).
void assumeSpurious(NodeId x, NodeId y)
Mark an arc as spurious: present in observationalBN, removed from causalDAG.
NodeSet parents(NodeId x) const
Parents 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
Base class for discrete random variable.
static NodeSetVec enumerateBackdoorSets(const DAG &dag, NodeId X, NodeId Y, const NodeSet &excluded_nodes=NodeSet(), std::size_t max_cardinality=0, bool only_minimal=true, bool stopAtFirst=false)
Enumerate valid backdoor adjustment sets.
static NodeSetVec enumerateFrontdoorSets(const DAG &dag, NodeId X, NodeId Y, const NodeSet &excluded_nodes=NodeSet(), std::size_t max_cardinality=0, bool only_minimal=true, bool stopAtFirst=false)
Enumerate valid frontdoor adjustment sets.
The class for generic Hash Tables.
Definition hashTable.h:640
value_type & insert(const Key &key, const Val &val)
Adds a new element (actually a copy of this element) into the hash table.
Exception: at least one argument passed to a function is not what was expected.
void setName(NodeId id, const std::string &name)
sets the name of node id
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
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
void erase(const Key &k)
Erases an element from the set.
Definition set_tpl.h:553
Door criteria (backdoor/frontdoor) utilities for a DAG.
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
Size NodeId
Type for node ids.
Set< NodeId > NodeSet
Some typdefs and define for shortcuts ...
bool contains(std::string_view s, std::string_view needle)
true if needle in s
gum is the global namespace for all aGrUM entities
Definition agrum.h:46
std::vector< LatentDescriptorIds > LatentDescriptorVector
Collection of latent descriptors (see LatentDescriptorIds).
Definition causalModel.h:66