Coverage for src/causalspyne/benchmark_fci.py: 100%
16 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-07-23 14:40 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-07-23 14:40 +0000
1"""
2Core logic for the paired FCI benchmark (root vs intermediate hidden).
4Factored out of examples/ so it can be unit-tested and reused.
5The key invariant: both scenarios are generated from the same integer seed,
6so they share the same DAG adjacency matrix and the same full data array;
7only the hidden column indices differ.
8"""
10from __future__ import annotations
12import numpy as np
14from causalspyne.main import gen_partially_observed
15from causalspyne.dag_gen_topo_order import RootConfounderDAG
18SCENARIO_HIDDEN = {
19 "root": [0], # topologically first confounder
20 "intermediate": [1.0], # topologically last confounder
21}
24def run_paired_scenarios(
25 seed: int,
26 num_macro_nodes: int = 4,
27 size_micro_node_dag=None,
28 max_num_local_nodes: int = 7,
29 min_num_local_nodes: int = 3,
30 degree: float = 2.0,
31 num_sample: int = 200,
32 output_dir: str = "/tmp/benchmark_fci",
33 strategy_cls=None,
34) -> dict:
35 """
36 Run both scenarios (root hidden, intermediate hidden) on the same DAG.
38 Returns a dict with keys 'root' and 'intermediate', each containing:
39 - 'subview': the DAGView object (observed data + metadata)
40 - 'full_data': np.ndarray of shape (num_sample, num_nodes_total)
41 — the data BEFORE any columns are hidden
42 - 'adj': binary adjacency matrix of the ground-truth DAG
43 - 'hidden': list of global node indices that were hidden
45 Invariant (tested in tests/test_benchmark_fci.py):
46 results['root']['adj'] == results['intermediate']['adj']
47 results['root']['full_data'] == results['intermediate']['full_data']
48 results['root']['hidden'] != results['intermediate']['hidden']
49 """
50 if strategy_cls is None:
51 strategy_cls = RootConfounderDAG
53 results = {}
54 for scenario_name, hidden_spec in SCENARIO_HIDDEN.items():
55 import io, contextlib
56 buf = io.StringIO()
57 with contextlib.redirect_stdout(buf):
58 subview = gen_partially_observed(
59 size_micro_node_dag=size_micro_node_dag,
60 max_num_local_nodes=max_num_local_nodes,
61 min_num_local_nodes=min_num_local_nodes,
62 num_macro_nodes=num_macro_nodes,
63 degree=degree,
64 list_confounder2hide=hidden_spec,
65 num_sample=num_sample,
66 output_dir=f"{output_dir}/{scenario_name}/seed_{seed}",
67 rng=seed, # integer → fresh RNG → reproducible
68 plot=False,
69 strategy_cls=strategy_cls,
70 )
72 results[scenario_name] = {
73 "subview": subview,
74 "full_data": subview._data_arr, # set by DAGView.run(), pre-hide
75 "adj": (subview.dag.mat_adjacency != 0).astype(int),
76 "hidden": list(subview.list_global_inds_nodes2hide),
77 }
79 return results