autocause 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. autocause/__init__.py +10 -0
  2. autocause/cli.py +607 -0
  3. autocause/config/defaults.json +244 -0
  4. autocause/core/__init__.py +6 -0
  5. autocause/core/causal_audit_adapter.py +176 -0
  6. autocause/core/causal_strength.py +573 -0
  7. autocause/core/ci_sensitivity.py +177 -0
  8. autocause/core/decision.py +418 -0
  9. autocause/core/diagnostics.py +895 -0
  10. autocause/core/distribution_tests.py +601 -0
  11. autocause/core/ensemble_scoring.py +277 -0
  12. autocause/core/evaluation_extras.py +247 -0
  13. autocause/core/experiment_tracker.py +499 -0
  14. autocause/core/falsification.py +505 -0
  15. autocause/core/graph_metrics.py +391 -0
  16. autocause/core/guards.py +75 -0
  17. autocause/core/icp_stability.py +738 -0
  18. autocause/core/io.py +460 -0
  19. autocause/core/lag_analysis.py +389 -0
  20. autocause/core/lagbands.py +366 -0
  21. autocause/core/methods/__init__.py +66 -0
  22. autocause/core/methods/consensus.py +775 -0
  23. autocause/core/methods/correlation.py +938 -0
  24. autocause/core/methods/granger.py +800 -0
  25. autocause/core/methods/lpcmci.py +190 -0
  26. autocause/core/methods/predictive_baseline.py +199 -0
  27. autocause/core/methods/tigramite_pcmci.py +1159 -0
  28. autocause/core/methods/transfer_entropy.py +518 -0
  29. autocause/core/methods/varlingam.py +207 -0
  30. autocause/core/methods/visualize_results.py +239 -0
  31. autocause/core/multiple_testing.py +66 -0
  32. autocause/core/output/__init__.py +5 -0
  33. autocause/core/output/edge_output.py +386 -0
  34. autocause/core/plot_cache.py +400 -0
  35. autocause/core/power_analysis.py +222 -0
  36. autocause/core/preprocessing.py +823 -0
  37. autocause/core/qc.py +315 -0
  38. autocause/core/robust_correlation.py +515 -0
  39. autocause/core/run_workflow.py +2593 -0
  40. autocause/core/sample_size_adequacy.py +494 -0
  41. autocause/core/sensitivity.py +609 -0
  42. autocause/core/stats.py +367 -0
  43. autocause/core/surrogate_inference.py +28 -0
  44. autocause/core/tau_max_estimation.py +828 -0
  45. autocause/core/tiered_consensus.py +423 -0
  46. autocause/core/validate.py +505 -0
  47. autocause/core/validation/__init__.py +71 -0
  48. autocause/core/validation/bootstrap.py +196 -0
  49. autocause/core/validation/cross_site.py +153 -0
  50. autocause/core/validation/effects.py +141 -0
  51. autocause/core/validation/link_classifier.py +107 -0
  52. autocause/core/validation/robustness.py +285 -0
  53. autocause/core/validation/stability.py +153 -0
  54. autocause/core/validation/surrogates.py +192 -0
  55. autocause/demo_interactive_summary.py +165 -0
  56. autocause/plots/__init__.py +67 -0
  57. autocause/plots/causal_analysis.py +914 -0
  58. autocause/plots/comparison.py +206 -0
  59. autocause/plots/correlation.py +534 -0
  60. autocause/plots/correlation_plots.py +650 -0
  61. autocause/plots/graphs.py +486 -0
  62. autocause/plots/interactive.py +760 -0
  63. autocause/plots/lags.py +217 -0
  64. autocause/plots/maps.py +211 -0
  65. autocause/plots/pvalues.py +180 -0
  66. autocause/reporting/__init__.py +40 -0
  67. autocause/reporting/causal_summary.py +483 -0
  68. autocause/reporting/summarize.py +350 -0
  69. autocause-0.2.1.dist-info/METADATA +232 -0
  70. autocause-0.2.1.dist-info/RECORD +73 -0
  71. autocause-0.2.1.dist-info/WHEEL +5 -0
  72. autocause-0.2.1.dist-info/licenses/LICENSE +661 -0
  73. autocause-0.2.1.dist-info/top_level.txt +1 -0
autocause/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Framework package for causal discovery in time series.
3
+ """
4
+
5
+ __version__ = "0.2.1"
6
+ __author__ = "Marco Ruiz, Miguel Arana-Catania, David R. Ardila, Rodrigo Ventura"
7
+
8
+ from . import core, plots, reporting, config
9
+
10
+ __all__ = ["core", "plots", "reporting", "config"]
autocause/cli.py ADDED
@@ -0,0 +1,607 @@
1
+ """
2
+ Command-Line Interface for Causal Discovery Framework
3
+
4
+ Provides easy-to-use CLI for running causal analysis pipelines,
5
+ generating visualizations, and producing reports.
6
+
7
+ Usage:
8
+ python -m framework.cli run --input data.csv --methods granger te pcmci consensus
9
+ python -m framework.cli plot --results results.csv --output plots/
10
+ python -m framework.cli report --experiment_name "Exp1" --results_dir results/
11
+ """
12
+
13
+ import argparse
14
+ import logging
15
+ import json
16
+ from pathlib import Path
17
+ import sys
18
+ import os
19
+
20
+ import pandas as pd
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Mitigate long Numba JIT compile stalls (e.g., from optional tigramite/numba paths)
25
+ # Set before any potential imports of libraries that use numba.
26
+ os.environ.setdefault("NUMBA_DISABLE_JIT", "1")
27
+ os.environ.setdefault("NUMBA_LOG_LEVEL", "WARNING")
28
+
29
+
30
+ def setup_logging(verbose: bool = False) -> None:
31
+ """Configure logging."""
32
+ level = logging.DEBUG if verbose else logging.INFO
33
+ logging.basicConfig(
34
+ level=level,
35
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
36
+ )
37
+
38
+
39
+ def run_command(args) -> int:
40
+ """Execute causal discovery run."""
41
+ logger.info(f"Running causal analysis on: {args.input}")
42
+
43
+ # Import here to avoid circular imports
44
+ from autocause.core.io import load_timeseries_csv
45
+ from autocause.core.qc import generate_qc_report
46
+ from autocause.core.methods import (
47
+ granger,
48
+ transfer_entropy,
49
+ tigramite_pcmci,
50
+ consensus,
51
+ )
52
+ from autocause.core.decision import decide_and_run as auto_decide_and_run
53
+
54
+ # Load data
55
+ try:
56
+ df, _meta = load_timeseries_csv(args.input)
57
+ logger.info(f"Loaded data: {df.shape}")
58
+ except Exception as e:
59
+ logger.error(f"Failed to load data: {e}")
60
+ return 1
61
+
62
+ # Quality control
63
+ qc_report = generate_qc_report(df)
64
+ logger.info(f"QC Report: {qc_report}")
65
+
66
+ # Get variable pairs
67
+ if args.pairs:
68
+ pairs = json.loads(args.pairs)
69
+ else:
70
+ # All combinations
71
+ vars_list = list(df.columns)
72
+ pairs = [
73
+ (vars_list[i], vars_list[j])
74
+ for i in range(len(vars_list))
75
+ for j in range(len(vars_list))
76
+ if i != j
77
+ ]
78
+
79
+ logger.info(f"Testing {len(pairs)} variable pairs")
80
+
81
+ results_dict = {}
82
+
83
+ # Run selected methods
84
+ if "granger" in args.methods:
85
+ logger.info("Running Granger causality...")
86
+ try:
87
+ results_dict["granger"] = granger.batch_granger_causality(
88
+ df, pairs, maxlag=args.maxlag, alpha=args.alpha
89
+ )
90
+ except Exception as e:
91
+ logger.error(f"Granger failed: {e}")
92
+
93
+ if "te" in args.methods:
94
+ logger.info("Running Transfer Entropy...")
95
+ try:
96
+ results_dict["transfer_entropy"] = transfer_entropy.batch_transfer_entropy(
97
+ df, pairs, delays=[1, 2, 3], alpha=args.alpha, method="discrete"
98
+ )
99
+ except Exception as e:
100
+ logger.error(f"Transfer Entropy failed: {e}")
101
+
102
+ if "pcmci" in args.methods:
103
+ logger.info("Running PCMCI+...")
104
+ try:
105
+ results_dict["pcmci"] = tigramite_pcmci.batch_pcmci(
106
+ df, pairs, tau_max=args.maxlag, alpha=args.alpha
107
+ )
108
+ except Exception as e:
109
+ logger.error(f"PCMCI+ failed: {e}")
110
+
111
+ if "auto" in args.methods:
112
+ logger.info("Running AUTO decision engine...")
113
+ try:
114
+ auto_df = auto_decide_and_run(
115
+ df, pairs, window_days=args.maxlag * 5, alpha=args.alpha
116
+ )
117
+ results_dict["auto"] = auto_df
118
+ # Also split by method for downstream compatibility
119
+ if (
120
+ auto_df is not None
121
+ and not auto_df.empty
122
+ and "method" in auto_df.columns
123
+ ):
124
+ g_df = auto_df[auto_df["method"] == "Granger"]
125
+ te_df = auto_df[auto_df["method"] == "TransferEntropy"]
126
+ pc_df = auto_df[auto_df["method"] == "PCMCI+"]
127
+ if not g_df.empty:
128
+ results_dict["granger"] = g_df
129
+ if not te_df.empty:
130
+ results_dict["transfer_entropy"] = te_df
131
+ if not pc_df.empty:
132
+ results_dict["pcmci"] = pc_df
133
+ except Exception as e:
134
+ logger.error(f"AUTO failed: {e}")
135
+
136
+ # Save results
137
+ output_dir = Path(args.output) if args.output else Path("results")
138
+ output_dir.mkdir(parents=True, exist_ok=True)
139
+
140
+ # Consensus if requested
141
+ if "consensus" in args.methods and len(results_dict) > 1:
142
+ logger.info("Computing consensus...")
143
+ try:
144
+ consensus_result = consensus.merge_method_results(
145
+ results_dict.get("granger"),
146
+ results_dict.get("transfer_entropy"),
147
+ results_dict.get("pcmci"),
148
+ min_votes=getattr(args, "min_votes", 2),
149
+ lag_tolerance_steps=getattr(args, "lag_tolerance_steps", 1),
150
+ sampling_days=getattr(args, "sampling_days", 1),
151
+ alpha=args.alpha,
152
+ output_dir=str(output_dir),
153
+ )
154
+ results_dict["consensus"] = consensus_result["consensus_edges"]
155
+ logger.info(
156
+ f"✅ Consensus: {len(consensus_result['consensus_edges'])} edges "
157
+ f"(saved CSV, GraphML, report)"
158
+ )
159
+ except Exception as e:
160
+ logger.error(f"Consensus failed: {e}")
161
+
162
+ # Save results
163
+
164
+ for method, results in results_dict.items():
165
+ if results is not None:
166
+ output_file = output_dir / f"{method}.csv"
167
+ results.to_csv(output_file, index=False)
168
+ logger.info(f"Saved: {output_file}")
169
+
170
+ logger.info(f"✅ Analysis complete. Results saved to: {output_dir}")
171
+ return 0
172
+
173
+
174
+ def plot_command(args) -> int:
175
+ """Generate visualizations."""
176
+ logger.info(f"Generating plots from: {args.results}")
177
+
178
+ from autocause.plots import (
179
+ plot_pvalue_distribution,
180
+ plot_lag_histogram,
181
+ plot_causal_graph,
182
+ plot_method_comparison,
183
+ )
184
+
185
+ output_dir = Path(args.output) if args.output else Path("figures")
186
+ output_dir.mkdir(parents=True, exist_ok=True)
187
+
188
+ # Load results
189
+ methods = ["granger", "transfer_entropy", "pcmci", "consensus"]
190
+ results_dict = {}
191
+
192
+ for method in methods:
193
+ result_file = (
194
+ Path(args.results) / f"{method}.csv"
195
+ if Path(args.results).is_dir()
196
+ else Path(args.results)
197
+ )
198
+ if result_file.exists():
199
+ results_dict[method] = pd.read_csv(result_file)
200
+ logger.info(f"Loaded: {result_file}")
201
+
202
+ if not results_dict:
203
+ logger.warning("No result files found")
204
+ return 1
205
+
206
+ # Generate plots
207
+ logger.info("Generating p-value distributions...")
208
+ for method, df in results_dict.items():
209
+ plot_pvalue_distribution(
210
+ df, method=method.upper(), output_path=output_dir / f"pvalues_{method}"
211
+ )
212
+
213
+ logger.info("Generating lag histograms...")
214
+ for method, df in results_dict.items():
215
+ lag_col = (
216
+ "delay" if "delay" in df.columns else "lag" if "lag" in df.columns else None
217
+ )
218
+ if lag_col:
219
+ plot_lag_histogram(
220
+ df,
221
+ method=method.upper(),
222
+ lag_column=lag_col,
223
+ output_path=output_dir / f"lags_{method}",
224
+ )
225
+
226
+ logger.info("Generating causal graphs...")
227
+ for method, df in results_dict.items():
228
+ if "source" in df.columns and "target" in df.columns:
229
+ plot_causal_graph(
230
+ df, method=method.upper(), output_path=output_dir / f"graph_{method}"
231
+ )
232
+
233
+ logger.info("Generating method comparison...")
234
+ plot_method_comparison(results_dict, output_path=output_dir / "method_comparison")
235
+
236
+ logger.info(f"✅ Plots saved to: {output_dir}")
237
+ return 0
238
+
239
+
240
+ def diagnose_command(args) -> int:
241
+ """Run diagnostic checks on data or results."""
242
+ logger.info(f"Running diagnostics: {args.experiment_name}")
243
+
244
+ from autocause.core.io import load_csv
245
+ from autocause.core.diagnostics import (
246
+ Diagnostics,
247
+ permutation_negative_control,
248
+ reverse_direction_check,
249
+ method_divergence_alert,
250
+ )
251
+ from autocause.plots import (
252
+ plot_pvalue_distribution,
253
+ plot_lag_histogram,
254
+ plot_method_comparison,
255
+ )
256
+
257
+ output_dir = Path(args.output) if args.output else Path("diagnostics")
258
+ output_dir.mkdir(parents=True, exist_ok=True)
259
+
260
+ # === Load results ===
261
+ results_dict = {}
262
+ if args.results:
263
+ results_path = Path(args.results)
264
+ if results_path.is_dir():
265
+ for csv_file in results_path.glob("*.csv"):
266
+ method = csv_file.stem
267
+ results_dict[method] = pd.read_csv(csv_file)
268
+ logger.info(f"Loaded: {csv_file}")
269
+ elif results_path.is_file():
270
+ results_dict["method"] = pd.read_csv(results_path)
271
+ logger.info(f"Loaded: {results_path}")
272
+
273
+ if not results_dict:
274
+ logger.warning("No result files found")
275
+ return 1
276
+
277
+ # === Run standard diagnostics ===
278
+ logger.info("\n" + "=" * 70)
279
+ logger.info("RUNNING STANDARD DIAGNOSTICS")
280
+ logger.info("=" * 70)
281
+
282
+ all_edges = pd.concat(results_dict.values(), ignore_index=True)
283
+ diag = Diagnostics(all_edges, cadence_days=args.cadence_days)
284
+ diag.run_all()
285
+
286
+ # Save standard diagnostics
287
+ diag.save_results(output_dir)
288
+
289
+ # === Generate plots ===
290
+ if args.plots:
291
+ logger.info("\n" + "=" * 70)
292
+ logger.info("GENERATING DIAGNOSTIC PLOTS")
293
+ logger.info("=" * 70)
294
+
295
+ figures_dir = output_dir / "figures"
296
+ figures_dir.mkdir(exist_ok=True)
297
+
298
+ # P-value distributions per method
299
+ for method, df in results_dict.items():
300
+ if "p_value" in df.columns:
301
+ try:
302
+ plot_pvalue_distribution(
303
+ df,
304
+ method=method.upper(),
305
+ output_path=figures_dir / f"pvalues_{method}",
306
+ )
307
+ logger.info(f" ✓ P-value distribution: {method}")
308
+ except Exception as e:
309
+ logger.warning(f" ✗ P-value plot failed for {method}: {e}")
310
+
311
+ # Lag distributions per method
312
+ for method, df in results_dict.items():
313
+ if "lag_days" in df.columns or "lag_steps" in df.columns:
314
+ try:
315
+ lag_col = "lag_days" if "lag_days" in df.columns else "lag_steps"
316
+ plot_lag_histogram(
317
+ df,
318
+ method=method.upper(),
319
+ lag_column=lag_col,
320
+ output_path=figures_dir / f"lags_{method}",
321
+ )
322
+ logger.info(f" ✓ Lag histogram: {method}")
323
+ except Exception as e:
324
+ logger.warning(f" ✗ Lag plot failed for {method}: {e}")
325
+
326
+ # Method comparison
327
+ try:
328
+ plot_method_comparison(
329
+ results_dict, output_path=figures_dir / "method_comparison"
330
+ )
331
+ logger.info(" ✓ Method comparison")
332
+ except Exception as e:
333
+ logger.warning(f" ✗ Method comparison failed: {e}")
334
+
335
+ # === Run advanced diagnostics ===
336
+ if args.advanced:
337
+ logger.info("\n" + "=" * 70)
338
+ logger.info("RUNNING ADVANCED DIAGNOSTICS")
339
+ logger.info("=" * 70)
340
+
341
+ advanced_results = {}
342
+
343
+ # Permutation negative control
344
+ if args.permutation and args.input:
345
+ logger.info("\n>>> Permutation Negative Control")
346
+ try:
347
+ df = load_csv(args.input)
348
+ # Get pairs from results
349
+ pairs = []
350
+ for _, row in all_edges.iterrows():
351
+ if "source" in row and "target" in row:
352
+ pairs.append((row["source"], row["target"]))
353
+ pairs = list(set(pairs)) # unique pairs
354
+
355
+ for method in results_dict.keys():
356
+ perm_result = permutation_negative_control(
357
+ df=df,
358
+ pairs=pairs,
359
+ method_name=method,
360
+ n_permutations=args.n_permutations,
361
+ alpha=args.alpha,
362
+ tolerance=args.tolerance,
363
+ maxlag=args.maxlag,
364
+ )
365
+ advanced_results[f"permutation_{method}"] = perm_result
366
+
367
+ except Exception as e:
368
+ logger.error(f"Permutation control failed: {e}")
369
+
370
+ # Reverse-direction check
371
+ if args.reverse and args.input:
372
+ logger.info("\n>>> Reverse-Direction Check")
373
+ try:
374
+ df = load_csv(args.input)
375
+
376
+ for method, edges_df in results_dict.items():
377
+ # Filter to significant edges only
378
+ if "is_significant" in edges_df.columns:
379
+ sig_edges = edges_df[edges_df["is_significant"] == 1]
380
+ elif "q_value" in edges_df.columns:
381
+ sig_edges = edges_df[edges_df["q_value"] < args.alpha]
382
+ elif "p_value" in edges_df.columns:
383
+ sig_edges = edges_df[edges_df["p_value"] < args.alpha]
384
+ else:
385
+ sig_edges = edges_df
386
+
387
+ if not sig_edges.empty:
388
+ reverse_result = reverse_direction_check(
389
+ df=df,
390
+ detected_edges=sig_edges,
391
+ method_name=method,
392
+ alpha=args.alpha,
393
+ tolerance=args.tolerance,
394
+ maxlag=args.maxlag,
395
+ )
396
+ advanced_results[f"reverse_{method}"] = reverse_result
397
+
398
+ except Exception as e:
399
+ logger.error(f"Reverse-direction check failed: {e}")
400
+
401
+ # Method divergence alert
402
+ if args.divergence:
403
+ logger.info("\n>>> Method Divergence Alert")
404
+ try:
405
+ divergence_result = method_divergence_alert(
406
+ results_dict=results_dict,
407
+ alpha=args.alpha,
408
+ divergence_threshold=args.divergence_threshold,
409
+ )
410
+ advanced_results["method_divergence"] = divergence_result
411
+ except Exception as e:
412
+ logger.error(f"Method divergence check failed: {e}")
413
+
414
+ # Save advanced diagnostics
415
+ if advanced_results:
416
+ import json
417
+
418
+ advanced_path = output_dir / "advanced_diagnostics.json"
419
+ with open(advanced_path, "w", encoding="utf-8") as f:
420
+ json.dump(advanced_results, f, indent=2, default=str)
421
+ logger.info(f"\n✅ Advanced diagnostics saved to: {advanced_path}")
422
+
423
+ logger.info(f"\n✅ Diagnostics complete. Results saved to: {output_dir}")
424
+ return 0
425
+
426
+
427
+ def report_command(args) -> int:
428
+ """Generate analysis reports."""
429
+ logger.info(f"Generating report for: {args.experiment_name}")
430
+
431
+ from autocause.reporting import (
432
+ generate_summary_report,
433
+ compare_with_baseline,
434
+ generate_latex_table,
435
+ )
436
+
437
+ output_dir = Path(args.output) if args.output else Path("reports")
438
+ output_dir.mkdir(parents=True, exist_ok=True)
439
+
440
+ # Load results
441
+ results_dict = {}
442
+ if args.results:
443
+ results_path = Path(args.results)
444
+ for csv_file in results_path.glob("*.csv"):
445
+ method = csv_file.stem
446
+ results_dict[method] = pd.read_csv(csv_file)
447
+ logger.info(f"Loaded: {csv_file}")
448
+
449
+ if not results_dict:
450
+ logger.warning("No result files found")
451
+ return 1
452
+
453
+ # Generate report
454
+ logger.info("Generating summary report...")
455
+ generate_summary_report(
456
+ results_dict,
457
+ experiment_name=args.experiment_name,
458
+ output_path=output_dir / "summary.txt",
459
+ )
460
+
461
+ # Generate LaTeX table
462
+ logger.info("Generating LaTeX table...")
463
+ comparison_df = compare_with_baseline(results_dict, args.experiment_name)
464
+ generate_latex_table(comparison_df, output_path=output_dir / "results_table.tex")
465
+
466
+ logger.info(f"✅ Reports saved to: {output_dir}")
467
+ return 0
468
+
469
+
470
+ def main() -> int:
471
+ """Main CLI entry point."""
472
+ parser = argparse.ArgumentParser(
473
+ description="Causal Discovery Framework CLI",
474
+ formatter_class=argparse.RawDescriptionHelpFormatter,
475
+ epilog="""
476
+ Examples:
477
+ # Run analysis
478
+ python -m framework.cli run --input data.csv --methods granger te pcmci consensus
479
+
480
+ # Generate plots
481
+ python -m framework.cli plot --results results/ --output figures/
482
+
483
+ # Generate report
484
+ python -m framework.cli report --experiment_name "Exp1" --results results/ --output reports/
485
+ """,
486
+ )
487
+
488
+ # Global arguments
489
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose logging")
490
+
491
+ # Subcommands
492
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
493
+
494
+ # RUN command
495
+ run_parser = subparsers.add_parser("run", help="Run causal analysis")
496
+ run_parser.add_argument("--input", required=True, help="Input CSV file")
497
+ run_parser.add_argument(
498
+ "--methods",
499
+ nargs="+",
500
+ default=["granger"],
501
+ choices=["granger", "te", "pcmci", "consensus", "auto"],
502
+ help="Methods to run",
503
+ )
504
+ run_parser.add_argument("--maxlag", type=int, default=12, help="Maximum lag")
505
+ run_parser.add_argument(
506
+ "--alpha", type=float, default=0.05, help="Significance level"
507
+ )
508
+ run_parser.add_argument("--pairs", help="JSON string of variable pairs")
509
+ run_parser.add_argument("--output", default="results", help="Output directory")
510
+ run_parser.set_defaults(func=run_command)
511
+
512
+ # PLOT command
513
+ plot_parser = subparsers.add_parser("plot", help="Generate visualizations")
514
+ plot_parser.add_argument(
515
+ "--results", required=True, help="Results directory or CSV file"
516
+ )
517
+ plot_parser.add_argument("--output", default="figures", help="Output directory")
518
+ plot_parser.set_defaults(func=plot_command)
519
+
520
+ # REPORT command
521
+ report_parser = subparsers.add_parser("report", help="Generate reports")
522
+ report_parser.add_argument(
523
+ "--experiment_name", required=True, help="Experiment name"
524
+ )
525
+ report_parser.add_argument("--results", help="Results directory")
526
+ report_parser.add_argument("--output", default="reports", help="Output directory")
527
+ report_parser.set_defaults(func=report_command)
528
+
529
+ # DIAGNOSE command
530
+ diagnose_parser = subparsers.add_parser(
531
+ "diagnose", help="Run diagnostic checks on results"
532
+ )
533
+ diagnose_parser.add_argument(
534
+ "--experiment_name", required=True, help="Experiment name"
535
+ )
536
+ diagnose_parser.add_argument(
537
+ "--results", required=True, help="Results directory or CSV file"
538
+ )
539
+ diagnose_parser.add_argument(
540
+ "--input", help="Input data CSV (required for permutation/reverse checks)"
541
+ )
542
+ diagnose_parser.add_argument(
543
+ "--output", default="diagnostics", help="Output directory"
544
+ )
545
+ diagnose_parser.add_argument(
546
+ "--cadence_days", type=int, default=5, help="Days per timestep"
547
+ )
548
+ diagnose_parser.add_argument(
549
+ "--alpha", type=float, default=0.05, help="Significance level"
550
+ )
551
+ diagnose_parser.add_argument(
552
+ "--plots", action="store_true", help="Generate diagnostic plots"
553
+ )
554
+ diagnose_parser.add_argument(
555
+ "--advanced", action="store_true", help="Run advanced diagnostics"
556
+ )
557
+ diagnose_parser.add_argument(
558
+ "--permutation",
559
+ action="store_true",
560
+ help="Run permutation negative control (requires --input)",
561
+ )
562
+ diagnose_parser.add_argument(
563
+ "--reverse",
564
+ action="store_true",
565
+ help="Run reverse-direction check (requires --input)",
566
+ )
567
+ diagnose_parser.add_argument(
568
+ "--divergence",
569
+ action="store_true",
570
+ help="Check for method divergence",
571
+ )
572
+ diagnose_parser.add_argument(
573
+ "--n_permutations", type=int, default=100, help="Number of permutations"
574
+ )
575
+ diagnose_parser.add_argument(
576
+ "--tolerance", type=float, default=0.03, help="Tolerance for FDR deviation"
577
+ )
578
+ diagnose_parser.add_argument(
579
+ "--divergence_threshold",
580
+ type=float,
581
+ default=0.20,
582
+ help="Relative divergence threshold",
583
+ )
584
+ diagnose_parser.add_argument(
585
+ "--maxlag", type=int, default=12, help="Maximum lag for methods"
586
+ )
587
+ diagnose_parser.set_defaults(func=diagnose_command)
588
+
589
+ # Parse arguments
590
+ args = parser.parse_args()
591
+
592
+ setup_logging(args.verbose)
593
+
594
+ if not args.command:
595
+ parser.print_help()
596
+ return 0
597
+
598
+ # Execute command
599
+ try:
600
+ return args.func(args)
601
+ except Exception as e:
602
+ logger.error(f"Error: {e}", exc_info=args.verbose)
603
+ return 1
604
+
605
+
606
+ if __name__ == "__main__":
607
+ sys.exit(main())