"""
Configuration Generator Module (Interactive CLI)
This file contains a standalone interactive command-line interface (CLI)
designed to generate a properly formatted JSON configuration file (`fc_config.json`)
for the PyFC (Feldman-Cousins) orchestrator. It handles user input collection,
type casting, rule validation, and default parameter fallback.
While this script does not perform mathematical calculations directly, it defines
the statistical hyper-parameters (such as Confidence Levels, likelihood formulations,
and Monte Carlo toy counts) that dictate the rigorousness and underlying assumptions
of the frequentist confidence interval construction.
Usage (Command Line Interface):
-------------------------------
To launch the interactive configuration generator, run the script directly from
your terminal:
$ python generate_config.py
Alternatively, if the PyFC package is installed in your Python environment:
$ python -m pyfc.generate_config
Interactive Instructions:
- You will be prompted sequentially for various configuration values.
- Type your desired value and press [Enter].
- To accept the default value displayed in brackets (e.g., [Default: binned]),
simply press [Enter] without typing anything.
- For boolean (yes/no) questions, accepted inputs include 'y', 'yes', 't', 'true', '1'
for True, and 'n', 'no', 'f', 'false', '0' for False (case-insensitive).
- For list inputs (like Confidence Levels), provide values separated by commas
(e.g., 0.68, 0.90, 0.95).
Created: v0.1.0 (July 24, 2026)
Author: Mauricio Bustamante (mbustamante@gmail.com)
This file was released as part of the PyFC code, stored at
https://github.com/mbustama/FeldmanCousins, which exists under a GNU GPL v3 License.
"""
import json
import os
import sys
[docs]
def parse_bool(value):
"""
Safely converts string inputs to boolean values.
Statistical Context:
Boolean flags in the configuration control critical methodological choices,
such as whether to apply the finite Monte Carlo correction (which shifts the
likelihood from a standard Poisson distribution to a Negative Binomial
Poisson-Gamma mixture) or whether to use adaptive sampling for the toys.
Parameters:
-----------
value : str or bool
The raw input provided by the user (e.g., 'y', 'n', 'true', '1').
Returns:
--------
bool
The parsed boolean True or False.
Raises:
-------
ValueError:
If the input string cannot be mapped to a valid boolean state.
"""
if isinstance(value, bool):
return value
val_lower = str(value).strip().lower()
if val_lower in ['true', 't', 'yes', 'y', '1']:
return True
elif val_lower in ['false', 'f', 'no', 'n', '0']:
return False
else:
raise ValueError("Please enter 'y' for True or 'n' for False.")
[docs]
def parse_float_list(value):
"""
Converts a comma-separated string to a list of floats.
Statistical Context:
This is primarily used to parse the Confidence Levels (CL). In the
Feldman-Cousins approach, the confidence level (1 - alpha) determines the
critical value of the Profile Likelihood Ratio test statistic. The framework
will compute intervals ensuring exact frequentist coverage at these specified
probabilities (e.g., 0.68 for 1-sigma, 0.90 for 90% CL).
Parameters:
-----------
value : str or list
The raw input string containing comma-separated numbers (e.g., "0.68, 0.90").
Returns:
--------
list of float
A list of floating-point numbers parsed from the input.
"""
if isinstance(value, list):
return value
return [float(x.strip()) for x in str(value).split(',')]
[docs]
def parse_str_list(value):
"""
Converts a comma-separated string to a list of strings.
Parameters:
-----------
value : str or list
The raw input string containing comma-separated names.
Returns:
--------
list of str
A list of stripped string values (typically representing physical parameter names).
"""
if isinstance(value, list):
return value
return [str(x.strip()) for x in str(value).split(',')]
[docs]
def main():
"""
Main entry point for the configuration generator.
Statistical Theory & Configuration Impact:
This sequence builds the `fc_config.json` matrix. It defines:
- PDF formulation (Binned vs Unbinned) which changes the NLL definition.
- Confidence Levels (CL) which sets the coverage integration targets.
- Monte Carlo sampling size (n_toys), controlling the resolution of the empirical
test statistic PDF. Higher toys reduce statistical noise in the p-value calculation
at the expense of computation time.
- Likelihood optimization strategies (SciPy, UltraNest, etc.) used to find the global
and conditional minima of the parameter space during profiling.
- Advanced topological settings (adaptive toys, grid sparsification) which employ
heuristics to skip unnecessary toy generations in regions definitively outside
the targeted confidence bounds.
Parameters:
-----------
None
Returns:
--------
None
Outputs a serialized JSON file containing the validated configuration.
"""
print("="*60)
print(" Feldman-Cousins Configuration Generator")
print(" Press [Enter] on any question to accept the default value.")
print("="*60 + "\n")
config = {}
# --- Physics & Stats Constraints ---
config["likelihood_type"] = get_input(
"1. Likelihood Type?",
default_val="binned",
cast_func=str,
choices=["binned", "unbinned"]
)
config["cl"] = get_input(
"2. Confidence Levels (comma-separated)?",
default_val="0.68, 0.90",
cast_func=parse_float_list,
validator=lambda lst: all(0.0 < x < 1.0 for x in lst),
error_msg="Confidence levels must be floats between 0.0 and 1.0."
)
config["n_toys"] = get_input(
"3. Number of Toys to generate?",
default_val=500,
cast_func=int,
validator=lambda x: x > 0,
error_msg="Number of toys must be a positive integer."
)
# --- Optimizer Configurations ---
config["strategy"] = get_input(
"4. Optimizer Strategy?",
default_val="scipy",
cast_func=str,
choices=["scipy", "ultranest", "hybrid", "grid"]
)
config["scipy_method"] = get_input(
"5. Override scipy.optimize.minimize's method ('auto' picks L-BFGS-B, or SLSQP if constraints are supplied programmatically)?",
default_val="auto",
cast_func=str,
choices=["auto", "L-BFGS-B", "SLSQP", "trust-constr"]
)
# Map the "auto" sentinel back to None for the orchestrator
if config["scipy_method"] == "auto":
config["scipy_method"] = None
config["use_finite_mc_correction_binned"] = get_input(
"6. Use finite MC correction for binned likelihoods (y/n)?",
default_val="y",
cast_func=parse_bool
)
# --- Compute Modes ---
config["compute_1D_intervals"] = get_input(
"7. Compute 1D Intervals (y/n)?",
default_val="y",
cast_func=parse_bool
)
config["compute_2D_intervals"] = get_input(
"8. Compute 2D Intervals (y/n)?",
default_val="y",
cast_func=parse_bool
)
# --- Hardware & Execution Setup ---
config["num_cores"] = get_input(
"9. Number of CPU cores to use (enter 0 for maximum available)?",
default_val=0,
cast_func=int,
validator=lambda x: x >= 0,
error_msg="Number of cores must be 0 or a positive integer."
)
# Map 0 back to None for the orchestrator
if config["num_cores"] == 0:
config["num_cores"] = None
config["verbose"] = get_input(
"10. Verbosity level (0=Silent, 1=Standard, 2=Detailed)?",
default_val=1,
cast_func=int,
choices=[0, 1, 2]
)
config["warm_start"] = get_input(
"11. Enable Warm Start / Checkpointing (y/n)?",
default_val="y",
cast_func=parse_bool
)
# --- Plotting & Visuals ---
config["param_names"] = get_input(
"12. Parameter names for plotting (comma-separated)?",
default_val="param1, param2, param3",
cast_func=parse_str_list
)
config["smooth_1d"] = get_input(
"13. Apply smoothing to 1D corner plot contours (y/n)?",
default_val="y",
cast_func=parse_bool
)
config["smooth_2d"] = get_input(
"14. Apply smoothing to 2D corner plot contours (y/n)?",
default_val="y",
cast_func=parse_bool
)
# --- Advanced Optimizer Flags ---
config["adaptive_toys"] = get_input(
"15. Enable Adaptive Toys (y/n)?",
default_val="y",
cast_func=parse_bool
)
config["toy_batch_size"] = get_input(
"16. Toy generation batch size?",
default_val=200,
cast_func=int,
validator=lambda x: x > 0,
error_msg="Batch size must be a positive integer."
)
config["sparsify_grid"] = get_input(
"17. Enable Grid Sparsification (y/n)?",
default_val="n",
cast_func=parse_bool
)
config["n_restarts"] = get_input(
"18. Number of restarts per DATA fit (scipy strategy only; keeps the lowest-NLL result)?",
default_val=1,
cast_func=int,
validator=lambda x: x > 0,
error_msg="Number of restarts must be a positive integer."
)
config["neighbor_seeding"] = get_input(
"19. Seed each DATA fit from an adjacent grid point's profiled parameters (y/n)?",
default_val="y",
cast_func=parse_bool
)
# --- File I/O ---
config["save_log"] = get_input(
"20. Save output to a log file (y/n)?",
default_val="y",
cast_func=parse_bool
)
config["save_directory"] = get_input(
"21. Output directory path?",
default_val="output/example_fc_output",
cast_func=str
)
config["output_file"] = get_input(
"22. Output file prefix (without extension)?",
default_val="fc_results",
cast_func=str
)
out_json_path = get_input(
"23. Path to save this configuration file to?",
default_val="fc_config.json",
cast_func=str
)
# --- Finalize and Export ---
print("="*60)
print(" Validating and generating configuration matrix...")
try:
# Ensure parent directories for the config file exist
parent_dir = os.path.dirname(out_json_path)
if parent_dir:
os.makedirs(parent_dir, exist_ok=True)
with open(out_json_path, 'w') as f:
json.dump(config, f, indent=4)
print(f" SUCCESS: Configuration successfully saved to -> {out_json_path}")
except Exception as e:
print(f" ERROR: Failed to write configuration file: {e}")
if __name__ == "__main__":
main()