11"""This module contains the main entrypoint functionality to the Causal Testing Framework."""
22
33import argparse
4+ import json
45import logging
56from enum import Enum
67from importlib .metadata import entry_points
78from typing import Optional , Sequence
9+ from warnings import warn
810
911import networkx as nx
1012import pandas as pd
1113
1214from causal_testing .causal_testing_framework import CausalTestingFramework , read_dataframe
13- from causal_testing .testing . metamorphic_relation import generate_causal_tests
15+ from causal_testing .specification . causal_dag import CausalDAG
1416
1517logger = logging .getLogger (__name__ )
1618
@@ -23,6 +25,7 @@ class Command(Enum):
2325 TEST = "test"
2426 GENERATE = "generate"
2527 DISCOVER = "discover"
28+ EVALUATE = "evaluate"
2629
2730
2831def setup_logging (level : str ) -> None :
@@ -40,15 +43,6 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
4043 "A causal inference-driven framework for functional black-box testing of complex software." ,
4144 )
4245
43- main_parser .add_argument (
44- "-l" ,
45- "--log_level" ,
46- default = "WARNING" ,
47- type = str .upper ,
48- choices = ["NONE" , "DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ],
49- help = "Set the logging level (default: WARNING)." ,
50- )
51-
5246 subparsers = main_parser .add_subparsers (
5347 help = "The action you want to run - call `causal_testing {action} -h` for further details" , dest = "command"
5448 )
@@ -57,24 +51,6 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
5751 parser_generate = subparsers .add_parser (Command .GENERATE .value , help = "Generate causal tests from a DAG" )
5852 parser_generate .add_argument ("-D" , "--dag-path" , help = "Path to the DAG file (.dot)" , required = True )
5953 parser_generate .add_argument ("-o" , "--output" , help = "Path for output file (.json)" , required = True )
60- parser_generate .add_argument (
61- "-e" ,
62- "--estimator" ,
63- help = "The name of the estimator class to use when evaluating tests (defaults to LinearRegressionEstimator)" ,
64- default = "LinearRegressionEstimator" ,
65- )
66- parser_generate .add_argument (
67- "-T" ,
68- "--effect-type" ,
69- help = "The effect type to estimate {direct, total}" ,
70- default = "direct" ,
71- )
72- parser_generate .add_argument (
73- "-E" ,
74- "--estimate-type" ,
75- help = "The estimate type to use when evaluating tests (defaults to coefficient)" ,
76- default = "coefficient" ,
77- )
7854 parser_generate .add_argument (
7955 "-i" , "--ignore-cycles" , help = "Ignore cycles in DAG" , action = "store_true" , default = False
8056 )
@@ -87,11 +63,10 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
8763 parser_test .add_argument ("-D" , "--dag-path" , help = "Path to the DAG file (.dot)" , required = True )
8864 parser_test .add_argument ("-o" , "--output" , help = "Path for output file (.json)" , required = True )
8965 parser_test .add_argument ("-i" , "--ignore-cycles" , help = "Ignore cycles in DAG" , action = "store_true" , default = False )
90- parser_test .add_argument ("-d" , "--data-paths" , help = "Paths to data files (.csv)" , nargs = "+" , required = True )
9166 parser_test .add_argument ("-t" , "--test-config" , help = "Path to test configuration file (.json)" , required = True )
9267 parser_test .add_argument ("-q" , "--query" , help = "Query string to filter data (e.g. 'age > 18')" , type = str )
9368 parser_test .add_argument (
94- "-a " , "--adequacy" , help = "Calculate causal test adequacy for each test case" , action = "store_true" , default = False
69+ "-A " , "--adequacy" , help = "Calculate causal test adequacy for each test case" , action = "store_true" , default = False
9570 )
9671 parser_test .add_argument (
9772 "-b" ,
@@ -108,18 +83,35 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
10883 default = False ,
10984 )
11085
86+ # DAG evaluation
87+ parser_evaluate = subparsers .add_parser (
88+ Command .EVALUATE .value , help = "Evaluate how well a causal DAG fits a dataset"
89+ )
90+ parser_evaluate .add_argument ("-D" , "--dag-path" , help = "Path to the DAG file (.dot)" , required = True )
91+ parser_evaluate .add_argument ("-o" , "--output" , help = "Path for output file (.csv)" , required = True )
92+ parser_evaluate .add_argument (
93+ "-i" , "--ignore-cycles" , help = "Ignore cycles in DAG" , action = "store_true" , default = False
94+ )
95+ parser_evaluate .add_argument ("-q" , "--query" , help = "Query string to filter data (e.g. 'age > 18')" , type = str )
96+ parser_evaluate .add_argument (
97+ "-b" ,
98+ "--adequacy-bootstrap-size" ,
99+ dest = "bootstrap_size" ,
100+ help = "Number of bootstrap samples for causal test adequacy. Defaults to 100" ,
101+ type = int ,
102+ default = 100 ,
103+ )
104+ parser_evaluate .add_argument (
105+ "-s" ,
106+ "--silent" ,
107+ action = "store_true" ,
108+ help = "Do not crash on error. If set to true, errors are recorded as test results." ,
109+ default = False ,
110+ )
111+ parser_evaluate .add_argument ("-t" , "--test-config" , help = "Path to test configuration file (.json)" )
112+
111113 # Discovery
112114 parser_discover = subparsers .add_parser (Command .DISCOVER .value , help = "Discover causal structures from data" )
113- parser_discover .add_argument ("-d" , "--data-paths" , help = "Paths to data files (.csv)" , nargs = "+" , required = True )
114- parser_discover .add_argument (
115- "-a" ,
116- "--alpha" ,
117- help = (
118- "The significance level of the confidence intervals used to determine causality. "
119- "This should be a value between 0 and 1. Defaults to 0.05 for 95%% confidence intervals."
120- ),
121- default = 0.05 ,
122- )
123115 parser_discover .add_argument (
124116 "-t" ,
125117 "--technique" ,
@@ -147,6 +139,26 @@ def parse_args(args: Optional[Sequence[str]] = None) -> argparse.Namespace:
147139 default = [],
148140 )
149141
142+ for parser in [parser_generate , parser_discover , parser_test , parser_evaluate ]:
143+ parser .add_argument (
144+ "-l" ,
145+ "--log_level" ,
146+ default = "WARNING" ,
147+ type = str .upper ,
148+ choices = ["NONE" , "DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ],
149+ help = "Set the logging level (default: WARNING)." ,
150+ )
151+ parser .add_argument (
152+ "-a" ,
153+ "--alpha" ,
154+ help = (
155+ "The significance level of the confidence intervals used to determine causality. "
156+ "This should be a value between 0 and 1. Defaults to 0.05 for 95%% confidence intervals."
157+ ),
158+ default = 0.05 ,
159+ )
160+ parser .add_argument ("-d" , "--data-paths" , help = "Paths to data files (.csv)" , nargs = "+" , required = True )
161+
150162 args = main_parser .parse_args (args )
151163
152164 # Assume the user wants test adequacy if they're setting bootstrap_size
@@ -175,16 +187,14 @@ def main() -> None:
175187 match args .command :
176188 case Command .GENERATE :
177189 logging .info ("Generating causal tests" )
178- generate_causal_tests (
179- args .dag_path ,
180- args .output ,
181- args .ignore_cycles ,
182- args .threads ,
183- effect_type = args .effect_type ,
184- estimate_type = args .estimate_type ,
185- estimator = args .estimator ,
190+ df = pd .concat (read_dataframe (path ) for path in args .data_paths )
191+ causal_dag = CausalDAG (args .dag_path , ignore_cycles = args .ignore_cycles , datatypes = df .dtypes )
192+ causal_tests = causal_dag .generate_causal_tests (
193+ threads = args .threads ,
186194 skip = False ,
187195 )
196+ with open (args .output , "w" , encoding = "utf-8" ) as f :
197+ json .dump ({"tests" : [test .to_dict () for test in causal_tests ]}, f )
188198 logging .info ("Causal test generation completed successfully." )
189199
190200 case Command .DISCOVER :
@@ -211,8 +221,8 @@ def main() -> None:
211221 # Drop unnamed columns
212222 unnamed_columns = [c for c in df .columns if c .startswith ("Unnamed: " )]
213223 if unnamed_columns :
214- logger . warning (f"Dropping unnamed columns: { unnamed_columns } " )
215- df = df .drop (unnamed_columns )
224+ warn (f"Dropping unnamed columns: { unnamed_columns } " )
225+ df = df .drop (unnamed_columns , axis = 1 )
216226
217227 discover_class = discover_map [args .technique ].load ()
218228 discover = discover_class (
@@ -246,6 +256,23 @@ def main() -> None:
246256 framework .save_results (args .output )
247257
248258 logging .info ("Causal testing completed successfully." )
259+ case Command .EVALUATE :
260+ # Create and setup framework
261+ framework = CausalTestingFramework ()
262+ framework .load_data (args .data_paths , query = args .query )
263+ framework .load_dag (args .dag_path , args .ignore_cycles )
264+ framework .dag .datatypes = framework .df .dtypes
265+
266+ if args .test_config :
267+ framework .load_test_cases_from_json (args .test_config )
268+ else :
269+ framework .test_cases = framework .dag .generate_causal_tests ()
270+
271+ logging .info ("Running tests on entire dataset" )
272+ results = framework .evaluate_dag (alpha = args .alpha , bootstrap_size = args .bootstrap_size )
273+ logging .info ("Causal testing completed successfully." )
274+ logging .info ("Running tests on bootstrap samples" )
275+ results .to_csv (args .output )
249276
250277
251278if __name__ == "__main__" :
0 commit comments