Coverage for src/causalspyne/dag_gen.py: 100%
43 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-05-26 05:29 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-05-26 05:29 +0000
1"""
2concrete class to generate simple DAGs
4"""
6import warnings
8from causalspyne.erdo_renyi_plp import Erdos_Renyi_PLP
9from causalspyne.dag_interface import MatDAG
10from causalspyne.weight import WeightGenWishart
11from causalspyne.dag_manipulator import DAGManipulator
12from causalspyne.utils_random import coerce_rng
15class GenDAG:
16 def __init__(self, num_nodes, degree, obj_gen_weight=None,
17 rng=None, strategy_cls=None):
18 """
19 degree: expected degree for each node
20 strategy_cls: callable class that takes rng and returns a skeleton
21 generator; defaults to Erdos_Renyi_PLP
22 """
23 rng = coerce_rng(rng)
24 self.num_nodes = num_nodes
25 self.degree = degree
26 if strategy_cls is None:
27 strategy_cls = Erdos_Renyi_PLP
28 self.strategy_gen_dag = strategy_cls(rng)
29 self.obj_gen_weight = obj_gen_weight
30 if obj_gen_weight is None:
31 self.obj_gen_weight = WeightGenWishart(rng=rng)
32 self.dag_manipulator = None
33 self.rng = rng
35 def gen_dag(self, num_nodes=None, prefix="", *, target_num_confounder):
36 """
37 generate DAG and wrap it around with interface
38 """
39 if num_nodes is None:
40 num_nodes = self.num_nodes
41 mat_skeleton = self.strategy_gen_dag(num_nodes, self.degree)
43 mat_mask = (mat_skeleton != 0).astype(float)
44 mat_weight = self.obj_gen_weight.gen(num_nodes)
45 # Hardarmard product
46 mat_weighted_adjacency = mat_mask * mat_weight
48 dag = MatDAG(mat_weighted_adjacency, name_prefix=prefix, rng=self.rng)
49 self.dag_manipulator = DAGManipulator(dag,
50 self.obj_gen_weight, self.rng)
51 ind_arbitrary = dag.get_top_last()
52 counter = 0
53 for _ in range(dag.num_nodes):
54 flag_success = self.dag_manipulator.mk_confound(
55 ind_arbitrary_confound_input=ind_arbitrary)
56 if not flag_success:
57 counter += 1
58 if dag.num_confounder >= target_num_confounder:
59 break
60 # FIXME: it can be the new ind_arbitrary has been tried out already
61 ind_arbitrary = dag.climb(ind_arbitrary)
62 if ind_arbitrary is None:
63 break
64 num_confounder = len(dag.list_confounder)
65 if num_confounder < target_num_confounder and \
66 dag.num_nodes - target_num_confounder > 1:
67 warnings.warn(
68 f"\n failed to ensure {target_num_confounder} confounders for \
69 adjacency matrix \n{dag.mat_adjacency}, \
70 \n after {counter} failed trials, \
71 \n{num_confounder} confounders only")
72 return dag