diff --git a/docs/changes/2096.maintenance.md b/docs/changes/2096.maintenance.md new file mode 100644 index 0000000000..7b00802745 --- /dev/null +++ b/docs/changes/2096.maintenance.md @@ -0,0 +1 @@ +Consolidate usage of command line configuration; removed duplicated items and solved name conflicts (e.g. zenith vs zenith_angle). diff --git a/src/simtools/application_control.py b/src/simtools/application_control.py index ad2c8313a9..caa0100652 100644 --- a/src/simtools/application_control.py +++ b/src/simtools/application_control.py @@ -1,5 +1,6 @@ """Application control utilities for startup and shutdown simtools applications.""" +import inspect import logging import os import re @@ -9,6 +10,7 @@ import simtools.utils.general as gen from simtools import dependencies, version +from simtools.configuration import configurator from simtools.db import db_handler from simtools.io import io_handler from simtools.settings import config @@ -140,6 +142,83 @@ class ApplicationContext: io_handler: io_handler.IOHandler | None +def build_application( + application_path=None, + description=None, + add_arguments_function=None, + initialization_kwargs=None, + startup_kwargs=None, + usage=None, + epilog=None, + parse_function=None, +): + """ + Build and start an application using the standard simtools startup flow. + + Parameters + ---------- + application_path : str, optional + Application file path, typically ``__file__``. + If not provided, it is inferred from the caller module. + description : str, optional + Application description shown in the CLI help (reduced to first line). + If not provided, it is inferred from the caller module docstring. + add_arguments_function : callable, optional + Function receiving the application's ``CommandLineParser`` instance to register + application-specific arguments. If not provided, ``_add_arguments`` from the + caller module is used when available. + initialization_kwargs : dict, optional + Keyword arguments forwarded to ``Configurator.initialize``. + startup_kwargs : dict, optional + Keyword arguments forwarded to ``startup_application``. + usage : str, optional + CLI usage string. + epilog : str, optional + CLI epilog. + parse_function : callable, optional + Existing parser function returning ``(args_dict, db_config)``. If provided, + ``build_application`` delegates directly to ``startup_application`` with this + parser function. + + Returns + ------- + ApplicationContext + Application context returned by ``startup_application``. + """ + initialization_kwargs = initialization_kwargs or {} + startup_kwargs = startup_kwargs or {} + + if application_path is None or description is None or add_arguments_function is None: + caller_globals = inspect.currentframe().f_back.f_globals + if application_path is None: + application_path = caller_globals.get("__file__") + if description is None: + description = caller_globals.get("__doc__") + if add_arguments_function is None: + add_arguments_function = caller_globals.get("_add_arguments") + + if application_path is None: + raise ValueError("Missing application path; provide application_path explicitly.") + if description is None: + raise ValueError("Missing description; provide description explicitly.") + + if parse_function is not None: + return startup_application(parse_function, **startup_kwargs) + + def _parse(): + config_builder = configurator.Configurator( + label=get_application_label(application_path), + usage=usage, + description=get_module_description_line(description), + epilog=epilog, + ) + if add_arguments_function is not None: + add_arguments_function(config_builder.parser) + return config_builder.initialize(**initialization_kwargs) + + return startup_application(_parse, **startup_kwargs) + + def startup_application( parse_function, setup_io_handler=True, @@ -253,6 +332,34 @@ def main(): return Path(file_path).stem +def get_module_description_line(docstring): + """Return the first non-empty line from a docstring. + + Parameters + ---------- + docstring : str + Module docstring (typically from __doc__). + + Returns + ------- + str + First non-empty line from the docstring. + + Raises + ------ + ValueError + If docstring is None or empty. + """ + if not docstring: + raise ValueError("Missing or empty docstring") + + for line in docstring.splitlines(): + if line.strip(): + return line.strip() + + raise ValueError("Empty docstring (only whitespace)") + + def _resolve_model_version_to_latest_patch(args_dict, logger): """ Update model_version in args_dict to latest patch version if needed. diff --git a/src/simtools/applications/convert_all_model_parameters_from_simtel.py b/src/simtools/applications/convert_all_model_parameters_from_simtel.py index 06392cdf5b..a2ce7e0000 100644 --- a/src/simtools/applications/convert_all_model_parameters_from_simtel.py +++ b/src/simtools/applications/convert_all_model_parameters_from_simtel.py @@ -65,40 +65,33 @@ import numpy as np import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model import schema from simtools.io import ascii_handler from simtools.simtel import simtel_config_reader -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Convert all model parameters from sim_telarray", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--simtel_cfg_file", help="File name for sim_telarray configuration", type=str, required=True, ) - config.parser.add_argument( + parser.add_argument( "--simtel_telescope_name", help="Name of the telescope in the sim_telarray configuration file", type=str, required=True, ) - config.parser.add_argument( + parser.add_argument( "--skip_parameter", help="List of parameters to be skipped.", type=str, nargs="*", default=[], ) - return config.initialize(simulation_model=["telescope", "parameter_version"]) def read_simtel_config_file(args_dict, schema_file, camera_pixels=None): @@ -310,8 +303,10 @@ def print_list_of_files(args_dict, logger): def main(): - """Convert all simulation model parameters exported from sim_telarray format.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"simulation_model": ["telescope", "parameter_version"]}, + ) _parameters_not_in_simtel, _simtel_parameters = read_and_export_parameters( app_context.args, app_context.logger, app_context.io_handler diff --git a/src/simtools/applications/convert_geo_coordinates_of_array_elements.py b/src/simtools/applications/convert_geo_coordinates_of_array_elements.py index f07da82eb3..23aacbbc54 100644 --- a/src/simtools/applications/convert_geo_coordinates_of_array_elements.py +++ b/src/simtools/applications/convert_geo_coordinates_of_array_elements.py @@ -55,31 +55,25 @@ """ import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model.metadata_collector import MetadataCollector from simtools.layout import array_layout -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Print a list of array element positions", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input", help="list of array element positions", required=True, ) - config.parser.add_argument( + parser.add_argument( "--input_meta", help="meta data file associated to input data", type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--print", help="print list of positions in requested coordinate system", required=False, @@ -90,7 +84,7 @@ def _parse(): "mercator", ], ) - config.parser.add_argument( + parser.add_argument( "--export", help="export array element list to file (in requested coordinate system)", required=False, @@ -101,31 +95,32 @@ def _parse(): "mercator", ], ) - config.parser.add_argument( + parser.add_argument( "--select_assets", help="select a subset of assets (e.g., MSTN, LSTN)", required=False, default=None, nargs="+", ) - config.parser.add_argument( + parser.add_argument( "--skip_input_validation", help="skip input data validation against schema", default=False, required=False, action="store_true", ) - return config.initialize( - db_config=True, - output=True, - require_command_line=True, - simulation_model=["model_version", "parameter_version", "site"], - ) def main(): - """Print a list of array elements.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "output": True, + "require_command_line": True, + "simulation_model": ["model_version", "parameter_version", "site"], + }, + ) if app_context.args.get("input", "").endswith(".json"): site = app_context.args.get("site", None) diff --git a/src/simtools/applications/convert_model_parameter_from_simtel.py b/src/simtools/applications/convert_model_parameter_from_simtel.py index 3737f153df..58873444ec 100644 --- a/src/simtools/applications/convert_model_parameter_from_simtel.py +++ b/src/simtools/applications/convert_model_parameter_from_simtel.py @@ -36,39 +36,38 @@ """ import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.simtel.simtel_config_reader import SimtelConfigReader -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Convert simulation model parameter from sim_telarray to simtools format.", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--schema", help="Schema file for model parameter validation", required=True ) - config.parser.add_argument( + parser.add_argument( "--simtel_cfg_file", help="File name for sim_telarray configuration", type=str, required=True, ) - config.parser.add_argument( + parser.add_argument( "--simtel_telescope_name", help="Name of the telescope in the sim_telarray configuration file", type=str, required=True, ) - return config.initialize(simulation_model=["telescope", "parameter_version"], output=True) def main(): - """Convert simulation model parameter from sim_telarray to simtools format.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "simulation_model": ["telescope", "parameter_version"], + "output": True, + }, + startup_kwargs={"setup_io_handler": False}, + ) simtel_config_reader = SimtelConfigReader( schema_file=app_context.args["schema"], diff --git a/src/simtools/applications/db_add_file_to_db.py b/src/simtools/applications/db_add_file_to_db.py index 73659165f9..40f9ab0177 100644 --- a/src/simtools/applications/db_add_file_to_db.py +++ b/src/simtools/applications/db_add_file_to_db.py @@ -41,19 +41,13 @@ from pathlib import Path import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Add file to the DB.", - usage="simtools-add-file-to-db --file_name test_application.dat --db test-data", - ) - group = config.parser.add_mutually_exclusive_group(required=True) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--file_name", help=("The file name to upload. A list of files is also allowed."), @@ -66,19 +60,17 @@ def _parse(): type=Path, ) - config.parser.add_argument( + parser.add_argument( "--db", type=str, help=("The database to insert the files to."), ) - config.parser.add_argument( + parser.add_argument( "--test_db", help="Use sandbox database. Drop all data after the operation.", action="store_true", ) - return config.initialize(paths=False, db_config=True) - def collect_files_to_insert(args_dict, logger, db): """ @@ -163,8 +155,11 @@ def confirm_and_insert_files(files_to_insert, args_dict, db, logger): def main(): - """Add files to the database.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"paths": False, "db_config": True}, + startup_kwargs={"setup_io_handler": False}, + ) db = db_handler.DatabaseHandler() diff --git a/src/simtools/applications/db_add_simulation_model_from_repository_to_db.py b/src/simtools/applications/db_add_simulation_model_from_repository_to_db.py index 9fd1bbdef9..34bf763911 100644 --- a/src/simtools/applications/db_add_simulation_model_from_repository_to_db.py +++ b/src/simtools/applications/db_add_simulation_model_from_repository_to_db.py @@ -53,24 +53,20 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler, db_model_upload +from simtools.settings import config -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Add or update a model parameter database to the DB", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input_path", help="Path to simulation model repository.", type=Path, required=True, ) - config.parser.add_argument( + parser.add_argument( "--type", help="Type of data to be uploaded to the database.", type=str, @@ -79,19 +75,28 @@ def _parse(): choices=["model_parameters", "production_tables"], ) - args_dict, db_config = config.initialize(output=True, require_command_line=True, db_config=True) - if args_dict.get("db_simulation_model") and args_dict.get("db_simulation_model_version"): - # overwrite explicitly DB configuration - db_config["db_simulation_model"] = args_dict["db_simulation_model"] - db_config["db_simulation_model_version"] = args_dict["db_simulation_model_version"] - else: - raise ValueError("Both db_simulation_model and db_simulation_model_version are required.") - return args_dict, db_config - def main(): - """Add or update a model parameter database to the DB.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "output": True, + "require_command_line": True, + "db_config": True, + }, + startup_kwargs={"setup_io_handler": False}, + ) + + if app_context.args.get("db_simulation_model") and app_context.args.get( + "db_simulation_model_version" + ): + app_context.db_config["db_simulation_model"] = app_context.args["db_simulation_model"] + app_context.db_config["db_simulation_model_version"] = app_context.args[ + "db_simulation_model_version" + ] + config.load(app_context.args, app_context.db_config) + else: + raise ValueError("Both db_simulation_model and db_simulation_model_version are required.") db = db_handler.DatabaseHandler() diff --git a/src/simtools/applications/db_add_value_from_json_to_db.py b/src/simtools/applications/db_add_value_from_json_to_db.py index f3f879cadd..5bcb0cdb05 100644 --- a/src/simtools/applications/db_add_value_from_json_to_db.py +++ b/src/simtools/applications/db_add_value_from_json_to_db.py @@ -29,38 +29,33 @@ from pathlib import Path import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler from simtools.io import ascii_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), description="Add a new parameter to the DB." - ) - group = config.parser.add_mutually_exclusive_group(required=True) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--file_name", help="file to be added", type=str) group.add_argument( "--input_path", help="A directory with json files to upload to the DB.", type=Path, ) - config.parser.add_argument( + parser.add_argument( "--db_collection", help="DB collection to which to add new values.", required=True ) - config.parser.add_argument( + parser.add_argument( "--test_db", help="Use sandbox database. Drop all data after the operation.", action="store_true", ) - return config.initialize(db_config=True) def main(): - """Add value from JSON to database.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application(initialization_kwargs={"db_config": True}) if app_context.args.get("test_db", False): app_context.db_config["db_simulation_model_version"] = str(uuid.uuid4()) diff --git a/src/simtools/applications/db_generate_compound_indexes.py b/src/simtools/applications/db_generate_compound_indexes.py index 00049a7443..16cc41260b 100644 --- a/src/simtools/applications/db_generate_compound_indexes.py +++ b/src/simtools/applications/db_generate_compound_indexes.py @@ -13,29 +13,26 @@ Database name (use "all" for all databases) """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - description="Generate compound indexes for a specific database", - label=get_application_label(__file__), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--db_name", help="Database name", default=None, required=False, ) - return config.initialize(db_config=True) def main(): - """Generate compound indexes for the specified database.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": True}, + startup_kwargs={"setup_io_handler": False}, + ) db = db_handler.DatabaseHandler() diff --git a/src/simtools/applications/db_get_array_layouts_from_db.py b/src/simtools/applications/db_get_array_layouts_from_db.py index 15ac5380ac..f940a799a5 100644 --- a/src/simtools/applications/db_get_array_layouts_from_db.py +++ b/src/simtools/applications/db_get_array_layouts_from_db.py @@ -52,27 +52,21 @@ """ import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.array_model import ArrayModel from simtools.model.site_model import SiteModel -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Get list of array elements as defined in the db (array layout).", - ) - - input_group = config.parser.add_mutually_exclusive_group() +def _add_arguments(parser): + """Register application-specific command line arguments.""" + input_group = parser.add_mutually_exclusive_group() input_group.add_argument( "--list_available_layouts", help="List available layouts in the database.", action="store_true", required=False, ) - config.parser.add_argument( + parser.add_argument( "--coordinate_system", help="Coordinate system for the array layout.", type=str, @@ -80,9 +74,6 @@ def _parse(): default="ground", choices=["ground", "utm"], ) - return config.initialize( - db_config=True, simulation_model=["site", "layout", "model_version"], output=True - ) def _layout_from_db(args_dict): @@ -111,8 +102,14 @@ def _layout_from_db(args_dict): def main(): - """Get list of array layouts or list of elements for a given layout as defined in the db.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "layout", "model_version"], + "output": True, + }, + ) if app_context.args.get("list_available_layouts", False): if app_context.args.get("site", None) is None: diff --git a/src/simtools/applications/db_get_file_from_db.py b/src/simtools/applications/db_get_file_from_db.py index 32eb587437..dd8e783d79 100644 --- a/src/simtools/applications/db_get_file_from_db.py +++ b/src/simtools/applications/db_get_file_from_db.py @@ -33,32 +33,27 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Get file(s) from the DB.", - usage="simtools-get-file-from-db --file_name mirror_CTA-S-LST_v2020-04-07.dat", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--file_name", help="The name of the file(s) to be downloaded (single file or space-separated list).", type=str, nargs="+", required=True, ) - return config.initialize(db_config=True, output=True) def main(): - """Get file from database.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + usage="simtools-get-file-from-db --file_name mirror_CTA-S-LST_v2020-04-07.dat", + initialization_kwargs={"db_config": True, "output": True}, + ) db = db_handler.DatabaseHandler() try: diff --git a/src/simtools/applications/db_get_parameter_from_db.py b/src/simtools/applications/db_get_parameter_from_db.py index 592582e6fc..8743afc744 100644 --- a/src/simtools/applications/db_get_parameter_from_db.py +++ b/src/simtools/applications/db_get_parameter_from_db.py @@ -100,21 +100,15 @@ from pprint import pprint -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler from simtools.io import ascii_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Export a parameter entry from model parameter database."), - ) - - config.parser.add_argument("--parameter", help="Parameter name", type=str, required=True) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument("--parameter", help="Parameter name", type=str, required=True) + parser.add_argument( "--output_file", help=( "Output file name for writing the DB entry, or base name for ECSV export of " @@ -123,7 +117,7 @@ def _parse(): type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--export_model_file", help=( "Export parameter data. File-backed parameters are written as files; " @@ -132,7 +126,7 @@ def _parse(): action="store_true", required=False, ) - config.parser.add_argument( + parser.add_argument( "--export_model_file_as_table", help=( "Also export file-backed parameters as ECSV. Use together with " @@ -143,14 +137,16 @@ def _parse(): action="store_true", required=False, ) - return config.initialize( - db_config=True, simulation_model=["telescope", "parameter_version", "model_version"] - ) def main(): - """Get a parameter entry from DB for a specific telescope or a site.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "parameter_version", "model_version"], + }, + ) db = db_handler.DatabaseHandler() diff --git a/src/simtools/applications/db_inspect_databases.py b/src/simtools/applications/db_inspect_databases.py index c57f663e04..4a824a002a 100644 --- a/src/simtools/applications/db_inspect_databases.py +++ b/src/simtools/applications/db_inspect_databases.py @@ -5,32 +5,30 @@ Command line arguments ---------------------- -db_name (str, optional) +db_name (str) Database name (use "all" for all databases) """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), description="Inspect databases" - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--db_name", help="Database name", default="all", required=True, ) - return config.initialize(db_config=True) def main(): - """Inspect databases.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": True}, + startup_kwargs={"setup_io_handler": False}, + ) db = db_handler.DatabaseHandler() # databases without internal databases we don't have rights to modify diff --git a/src/simtools/applications/db_upload_model_repository.py b/src/simtools/applications/db_upload_model_repository.py index 6fcf4d3589..b777c88b2c 100644 --- a/src/simtools/applications/db_upload_model_repository.py +++ b/src/simtools/applications/db_upload_model_repository.py @@ -40,9 +40,9 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler, db_model_upload +from simtools.settings import config DEFAULT_REPOSITORY_URL = ( "https://gitlab.cta-observatory.org/cta-science/simulations/" @@ -50,19 +50,15 @@ ) -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Upload model parameters from repository to database", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--branch", help="Repository branch to clone (optional, defaults to using version tag).", type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--tmp_dir", help="Temporary directory for cloning the repository (default: ./tmp_model_parameters).", type=str, @@ -70,23 +66,28 @@ def _parse(): required=False, ) - args_dict, db_config = config.initialize(output=True, require_command_line=True, db_config=True) - if args_dict.get("db_simulation_model_version"): - db_config["db_simulation_model"] = args_dict.get( +def main(): + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "output": True, + "require_command_line": True, + "db_config": True, + }, + ) + + if app_context.args.get("db_simulation_model_version"): + app_context.db_config["db_simulation_model"] = app_context.args.get( "db_simulation_model", "CTAO-Simulation-Model" ) - db_config["db_simulation_model_version"] = args_dict["db_simulation_model_version"] + app_context.db_config["db_simulation_model_version"] = app_context.args[ + "db_simulation_model_version" + ] + config.load(app_context.args, app_context.db_config) else: raise ValueError("Setting of db_simulation_model_version is required.") - return args_dict, db_config - - -def main(): - """Application main.""" - app_context = startup_application(_parse) - db = db_handler.DatabaseHandler() db.print_connection_info() diff --git a/src/simtools/applications/derive_ctao_array_layouts.py b/src/simtools/applications/derive_ctao_array_layouts.py index ad97777761..09c199dc71 100644 --- a/src/simtools/applications/derive_ctao_array_layouts.py +++ b/src/simtools/applications/derive_ctao_array_layouts.py @@ -42,8 +42,7 @@ --updated_parameter_version 3.0.0 """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler from simtools.layout.array_layout_utils import ( merge_array_layouts, @@ -52,39 +51,38 @@ ) -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Derive CTAO array layouts from CTAO common identifiers repository.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--repository_url", help="URL or path of the CTAO common identifiers repository.", type=str, default="https://gitlab.cta-observatory.org/cta-computing/common/identifiers/-/raw/", ) - config.parser.add_argument( + parser.add_argument( "--repository_branch", help="Repository branch to use for CTAO common identifiers.", type=str, default="main", required=False, ) - config.parser.add_argument( + parser.add_argument( "--updated_parameter_version", help="Updated parameter version.", type=str, required=False, ) - return config.initialize( - db_config=True, output=True, simulation_model=["site", "parameter_version", "model_version"] - ) def main(): - """Derive CTAO array layouts from CTAO common identifiers repository.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "output": True, + "simulation_model": ["site", "parameter_version", "model_version"], + }, + ) ctao_array_layouts = retrieve_ctao_array_layouts( site=app_context.args["site"], diff --git a/src/simtools/applications/derive_incident_angle.py b/src/simtools/applications/derive_incident_angle.py index 1ce04134fc..6c543298ef 100644 --- a/src/simtools/applications/derive_incident_angle.py +++ b/src/simtools/applications/derive_incident_angle.py @@ -71,74 +71,53 @@ :width: 49 % """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator -from simtools.configuration.commandline_parser import CommandLineParser +import astropy.units as u + +from simtools.application_control import build_application from simtools.ray_tracing.incident_angles import IncidentAnglesCalculator from simtools.visualization.plot_incident_angles import plot_incident_angles -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Derive photon incident angles on focal plane and primary/secondary mirrors."), - ) - config.parser.add_argument( - "--off_axis_angles", - help="One or more off-axis angles in degrees (space-separated)", - type=float, - nargs="+", - required=False, - ) - config.parser.add_argument( - "--source_distance", - help="Source distance in kilometers", - type=float, - default=10.0, - required=False, +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments( + ["off_axis_angles", "source_distance", "number_of_photons"] ) - config.parser.add_argument( - "--number_of_photons", - help="Number of star photons to trace (per run)", - type=CommandLineParser.scientific_int, - default=10000, - required=False, - ) - config.parser.add_argument( + parser.add_argument( "--perfect_mirror", help="Assume perfect mirror shape/alignment/reflection", action="store_true", required=False, ) - config.parser.add_argument( + parser.add_argument( "--debug_plots", dest="debug_plots", help="Generate additional debug plots (radius histograms, XY heatmaps, radius vs angle)", action="store_true", required=False, ) - config.parser.add_argument( + parser.add_argument( "--calculate_primary_secondary_angles", dest="calculate_primary_secondary_angles", help="Compute angles of incidence on primary and secondary mirrors", required=False, action="store_true", ) - return config.initialize( - db_config=True, - simulation_model=["telescope", "site", "model_version"], - ) def main(): - """Derive photon incident angles on focal plane and primary/secondary mirrors.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "site", "model_version"], + }, + ) app_context.logger.info("Starting derivation of incident angles") output_dir = app_context.io_handler.get_output_directory() - base_label = app_context.args.get("label", get_application_label(__file__)) + base_label = app_context.args.get("label") or app_context.args["application_label"] telescope_name = app_context.args["telescope"] label_with_telescope = f"{base_label}_{telescope_name}" @@ -147,7 +126,9 @@ def main(): output_dir=output_dir, label=base_label, ) - offsets = [float(v) for v in app_context.args.get("off_axis_angles", [0.0])] + offsets = [ + value.to_value(u.deg) for value in app_context.args.get("off_axis_angles", [0.0 * u.deg]) + ] results_by_offset = calculator.run_for_offsets(offsets) plot_incident_angles( diff --git a/src/simtools/applications/derive_mirror_rnda.py b/src/simtools/applications/derive_mirror_rnda.py index 5fc90cc4f9..aaee643b4e 100644 --- a/src/simtools/applications/derive_mirror_rnda.py +++ b/src/simtools/applications/derive_mirror_rnda.py @@ -65,39 +65,34 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.ray_tracing.mirror_panel_psf import MirrorPanelPSF from simtools.ray_tracing.psf_parameter_optimisation import cleanup_intermediate_files -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - description="Derive mirror RNDA using per-mirror PSF diameter optimization.", - label=get_application_label(__file__), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--data", help="ECSV file with a PSF diameter column (mm) per mirror", type=str, required=True, ) - config.parser.add_argument( + parser.add_argument( "--threshold", help="Convergence threshold for percentage difference.", type=float, required=False, default=0.05, ) - config.parser.add_argument( + parser.add_argument( "--learning_rate", help="Learning rate for gradient descent.", type=float, required=False, default=0.001, ) - config.parser.add_argument( + parser.add_argument( "--fraction", help=( "PSF containment fraction for diameter calculation (e.g., 0.8 for D80, 0.95 for D95)." @@ -105,21 +100,21 @@ def _parse(): type=float, default=0.8, ) - config.parser.add_argument( + parser.add_argument( "--n_workers", help="Number of parallel worker processes to use.", type=int, required=False, default=0, ) - config.parser.add_argument( + parser.add_argument( "--number_of_mirrors_to_test", help="Number of mirrors to optimize when --test is used.", type=int, required=False, default=10, ) - config.parser.add_argument( + parser.add_argument( "--psf_hist", nargs="?", const="psf_distributions.png", @@ -129,7 +124,7 @@ def _parse(): "Optionally provide a filename (relative to output dir unless absolute)." ), ) - config.parser.add_argument( + parser.add_argument( "--cleanup", action="store_true", default=False, @@ -137,16 +132,17 @@ def _parse(): "Remove intermediate files from the output directory (patterns: *.log, *.lis*, *.dat)." ), ) - return config.initialize( - db_config=True, - output=True, - simulation_model=["telescope", "model_version", "site", "parameter_version"], - ) def main(): - """Derive mirror random reflection angle using per-mirror PSF diameter optimization.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "output": True, + "simulation_model": ["telescope", "model_version", "site", "parameter_version"], + }, + ) panel_psf = MirrorPanelPSF(app_context.args.get("label"), app_context.args) panel_psf.optimize_with_gradient_descent() panel_psf.write_optimization_data() diff --git a/src/simtools/applications/derive_photon_electron_spectrum.py b/src/simtools/applications/derive_photon_electron_spectrum.py index f653e05d60..26adb9acf8 100644 --- a/src/simtools/applications/derive_photon_electron_spectrum.py +++ b/src/simtools/applications/derive_photon_electron_spectrum.py @@ -36,51 +36,46 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application +from simtools.application_control import build_application from simtools.camera.single_photon_electron_spectrum import SinglePhotonElectronSpectrum -from simtools.configuration import configurator -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Derive single photon electron spectrum from a given amplitude spectrum.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input_spectrum", help="File with amplitude spectrum.", type=Path, required=True, ) - config.parser.add_argument( + parser.add_argument( "--afterpulse_spectrum", help="File with afterpulse spectrum.", type=Path, required=False, ) - config.parser.add_argument( + parser.add_argument( "--step_size", help="Step size in amplitude spectrum", type=float, default=0.02, required=False, ) - config.parser.add_argument( + parser.add_argument( "--max_amplitude", help="Maximum amplitude for single p.e. for amplitude spectrum", type=float, default=42.0, required=False, ) - config.parser.add_argument( + parser.add_argument( "--scale_afterpulse_spectrum", help="Scale afterpulse spectrum by the given factor", type=float, default=1.0, required=False, ) - config.parser.add_argument( + parser.add_argument( "--afterpulse_amplitude_range", help="Amplitude range in pe for afterpulse calculation", type=float, @@ -88,32 +83,36 @@ def _parse(): default=[0.0, 42.0], required=False, ) - config.parser.add_argument( + parser.add_argument( "--fit_afterpulse", help="Fit afterpulse spectrum with an exponential decay function.", action="store_true", required=False, ) - config.parser.add_argument( + parser.add_argument( "--afterpulse_decay_factor_fixed_value", help="Fix decay factor in afterpulse fit (free fit parameter if not set set).", type=float, default=15.0, required=False, ) - config.parser.add_argument( + parser.add_argument( "--use_norm_spe", help="Use sim_telarray tool 'norm_spe' to normalize the spectrum.", action="store_true", required=False, ) - return config.initialize(db_config=False, output=True, simulation_model=["telescope"]) - def main(): - """Derive single photon electron spectrum from a given amplitude spectrum.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": False, + "output": True, + "simulation_model": ["telescope"], + }, + ) single_pe = SinglePhotonElectronSpectrum(app_context.args) single_pe.derive_single_pe_spectrum() diff --git a/src/simtools/applications/derive_psf_parameters.py b/src/simtools/applications/derive_psf_parameters.py index 9bf6ecb025..5848892272 100644 --- a/src/simtools/applications/derive_psf_parameters.py +++ b/src/simtools/applications/derive_psf_parameters.py @@ -45,9 +45,9 @@ Model version. parameter_version (str, optional) Parameter version for model parameter file export. - src_distance (float, optional) + source_distance (float or quantity, optional) Source distance in km. - zenith (float, optional) + zenith_angle (float or quantity, optional) Zenith angle in deg. data (str, optional) Name of the data file with the measured cumulative PSF. @@ -107,31 +107,15 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.model_utils import initialize_simulation_models from simtools.ray_tracing import psf_parameter_optimisation as psf_opt -def _parse(): - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Derive mirror_align_random_horizontal and mirror_align_random_vertical " - "using cumulative PSF measurement." - ), - ) - config.parser.add_argument( - "--src_distance", - help="Source distance in km", - type=float, - default=10, - ) - config.parser.add_argument("--zenith", help="Zenith angle in deg", type=float, default=20) - config.parser.add_argument( - "--data", help="Data file name with the measured PSF vs radius [cm]", type=str - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["source_distance", "zenith_angle", "data"]) + parser.add_argument( "--plot_all", help=( "On: plot cumulative PSF for all tested combinations, " @@ -139,13 +123,13 @@ def _parse(): ), action="store_true", ) - config.parser.add_argument( + parser.add_argument( "--write_psf_parameters", help=("Write the optimized PSF parameters as simulation model parameter files"), action="store_true", required=False, ) - config.parser.add_argument( + parser.add_argument( "--rmsd_threshold", help=( "RMSD threshold for gradient descent convergence " @@ -154,7 +138,7 @@ def _parse(): type=float, default=0.01, ) - config.parser.add_argument( + parser.add_argument( "--learning_rate", help=( "Learning rate for gradient descent optimization " @@ -163,36 +147,37 @@ def _parse(): type=float, default=0.0001, ) - config.parser.add_argument( + parser.add_argument( "--monte_carlo_analysis", help="Run analysis to find monte carlo uncertainties.", action="store_true", ) - config.parser.add_argument( + parser.add_argument( "--ks_statistic", help="Use KS statistic for monte carlo uncertainty analysis.", action="store_true", ) - config.parser.add_argument( + parser.add_argument( "--fraction", help="PSF containment fraction for diameter calculation (e.g., 0.8 for D80, 0.95 for D95).", type=float, default=0.8, ) - config.parser.add_argument( + parser.add_argument( "--cleanup", help="Remove intermediate *.log and *.lis* files after optimization.", action="store_true", ) - return config.initialize( - db_config=True, - simulation_model=["telescope", "model_version", "parameter_version"], - ) def main(): - """Derive PSF parameters.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "model_version", "parameter_version"], + }, + ) tel_model, site_model, _ = initialize_simulation_models( label=app_context.args.get("label"), diff --git a/src/simtools/applications/derive_pulse_shape_parameters.py b/src/simtools/applications/derive_pulse_shape_parameters.py index ce0b50dbce..d10ac89057 100644 --- a/src/simtools/applications/derive_pulse_shape_parameters.py +++ b/src/simtools/applications/derive_pulse_shape_parameters.py @@ -52,34 +52,26 @@ import logging import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.model_utils import initialize_simulation_models from simtools.simtel.pulse_shapes import solve_sigma_tau_from_rise_fall -def _parse(): - """Parse command line configuration for parameter derivation.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Derive Gaussian sigma and exponential tau from rise/fall width specifications." - ), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--rise_width_ns", help="Wdth on the rising edge in ns between rise_range fractions.", type=float, required=True, ) - config.parser.add_argument( + parser.add_argument( "--fall_width_ns", help="Width on the falling edge in ns between fall_range fractions.", type=float, required=True, ) - config.parser.add_argument( + parser.add_argument( "--rise_range", help="Fractional amplitudes (low high) for rise width, e.g. 0.1 0.9", type=float, @@ -87,7 +79,7 @@ def _parse(): default=[0.1, 0.9], required=False, ) - config.parser.add_argument( + parser.add_argument( "--fall_range", help="Fractional amplitudes (high low) for fall width, e.g. 0.9 0.1", type=float, @@ -95,14 +87,14 @@ def _parse(): default=[0.9, 0.1], required=False, ) - config.parser.add_argument( + parser.add_argument( "--dt_ns", help="Time sampling step in ns used by the solver.", type=float, default=0.1, required=False, ) - config.parser.add_argument( + parser.add_argument( "--time_margin_ns", help=( "Margin (ns) added to both ends of the instrument readout window when deriving the " @@ -113,16 +105,16 @@ def _parse(): required=False, ) - return config.initialize( - db_config=True, - simulation_model=["site", "telescope", "model_version", "parameter_version"], - output=True, - ) - def main(): - """Run parameter derivation and write results.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "telescope", "model_version", "parameter_version"], + "output": True, + }, + ) log = logging.getLogger(__name__) rise_width_ns = app_context.args["rise_width_ns"] @@ -132,7 +124,7 @@ def main(): dt_ns = app_context.args["dt_ns"] time_margin_ns = app_context.args["time_margin_ns"] site = app_context.args["site"] - label = app_context.args.get("label") or get_application_label(__file__) + label = app_context.args.get("label") or app_context.args["application_label"] telescope_model, _, _ = initialize_simulation_models( label=label, model_version=app_context.args["model_version"], diff --git a/src/simtools/applications/derive_trigger_rates.py b/src/simtools/applications/derive_trigger_rates.py index 1177e645f9..154a649374 100644 --- a/src/simtools/applications/derive_trigger_rates.py +++ b/src/simtools/applications/derive_trigger_rates.py @@ -37,49 +37,40 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.telescope_trigger_rates import telescope_trigger_rates -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Derive trigger rates for a single telescope or an array of telescopes.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["telescope_ids"]) + parser.add_argument( "--event_data_file", type=str, required=True, help="Event data file containing reduced event data.", ) - config.parser.add_argument( - "--telescope_ids", - type=str, - required=False, - help="Path to a file containing telescope configurations.", - ) - config.parser.add_argument( + parser.add_argument( "--plot_histograms", help="Plot histograms of the event data.", action="store_true", default=False, ) - return config.initialize( - db_config=True, - output=True, - simulation_model=[ - "site", - "model_version", - "layout", - ], - ) def main(): - """Derive trigger rates for a single telescope or an array of telescopes.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "output": True, + "simulation_model": [ + "site", + "model_version", + "layout", + ], + }, + ) telescope_trigger_rates(app_context.args) diff --git a/src/simtools/applications/docs_produce_array_element_report.py b/src/simtools/applications/docs_produce_array_element_report.py index f41b9496af..46903224b8 100644 --- a/src/simtools/applications/docs_produce_array_element_report.py +++ b/src/simtools/applications/docs_produce_array_element_report.py @@ -9,49 +9,37 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.reporting.docs_auto_report_generator import ReportGenerator from simtools.reporting.docs_read_parameters import ReadParameters -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Produce a markdown report for model parameters."), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["all_model_versions"]) + parser.add_argument( "--all_telescopes", action="store_true", help="Produce reports for all telescopes.", ) - config.parser.add_argument( - "--all_model_versions", - action="store_true", - help="Produce reports for all model versions.", - ) + parser.add_argument("--all_sites", action="store_true", help="Produce reports for all sites.") - config.parser.add_argument( - "--all_sites", action="store_true", help="Produce reports for all sites." - ) - - config.parser.add_argument( + parser.add_argument( "--observatory", action="store_true", help="Produce reports for an observatory at a given site.", ) - return config.initialize( - db_config=True, simulation_model=["site", "telescope", "model_version"] - ) - def main(): - """Produce a markdown file for a given array element, site, and model version.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "telescope", "model_version"], + }, + ) output_path = app_context.io_handler.get_output_directory() if any( diff --git a/src/simtools/applications/docs_produce_calibration_reports.py b/src/simtools/applications/docs_produce_calibration_reports.py index c2281badc4..b0faa269b1 100644 --- a/src/simtools/applications/docs_produce_calibration_reports.py +++ b/src/simtools/applications/docs_produce_calibration_reports.py @@ -2,33 +2,20 @@ r"""Produces a markdown file for calibration reports.""" -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.reporting.docs_auto_report_generator import ReportGenerator -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Produce a markdown report for calibration parameters."), - ) - - config.parser.add_argument( - "--all_model_versions", - action="store_true", - help="Produce reports for all model versions.", - ) - - return config.initialize( - db_config=True, - simulation_model=["model_version"], - ) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["all_model_versions"]) def main(): - """Produce a markdown file for calibration reports.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": True, "simulation_model": ["model_version"]}, + ) output_path = app_context.io_handler.get_output_directory() diff --git a/src/simtools/applications/docs_produce_model_parameter_reports.py b/src/simtools/applications/docs_produce_model_parameter_reports.py index 5ffd416c72..3b5700b18f 100644 --- a/src/simtools/applications/docs_produce_model_parameter_reports.py +++ b/src/simtools/applications/docs_produce_model_parameter_reports.py @@ -8,35 +8,27 @@ Currently only implemented for telescopes. """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.reporting.docs_auto_report_generator import ReportGenerator from simtools.reporting.docs_read_parameters import ReadParameters -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Produce a markdown report for model parameters."), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--all_telescopes", action="store_true", help="Produce reports for all telescopes.", ) - config.parser.add_argument( - "--all_sites", action="store_true", help="Produce reports for all sites." - ) - - return config.initialize(db_config=True, simulation_model=["site", "telescope"]) + parser.add_argument("--all_sites", action="store_true", help="Produce reports for all sites.") def main(): - """Produce a model parameter report per array element.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": True, "simulation_model": ["site", "telescope"]}, + ) output_path = app_context.io_handler.get_output_directory() if any([app_context.args.get("all_telescopes"), app_context.args.get("all_sites")]): diff --git a/src/simtools/applications/docs_produce_simulation_configuration_report.py b/src/simtools/applications/docs_produce_simulation_configuration_report.py index b1f5e21939..da2a22db24 100644 --- a/src/simtools/applications/docs_produce_simulation_configuration_report.py +++ b/src/simtools/applications/docs_produce_simulation_configuration_report.py @@ -2,34 +2,24 @@ r"""Produces a markdown file for a given simulation configuration.""" -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.reporting.docs_auto_report_generator import ReportGenerator -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Produce a markdown report for model parameters."), - ) - - config.parser.add_argument( - "--all_model_versions", - action="store_true", - help="Produce reports for all model versions.", - ) - - return config.initialize( - db_config=True, - simulation_model=["model_version"], - simulation_configuration=["software"], - ) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["all_model_versions"]) def main(): - """Produce a markdown file for a given simulation configuration.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["model_version"], + "simulation_configuration": ["software"], + }, + ) output_path = app_context.io_handler.get_output_directory() diff --git a/src/simtools/applications/generate_array_config.py b/src/simtools/applications/generate_array_config.py index 9f35e61c0c..f81c2854c9 100644 --- a/src/simtools/applications/generate_array_config.py +++ b/src/simtools/applications/generate_array_config.py @@ -25,23 +25,18 @@ The output is saved in simtools-output/test/model. """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.array_model import ArrayModel -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Generate sim_telarray configuration files for a given array.", - ) - return config.initialize(db_config=True, simulation_model=["site", "layout", "model_version"]) - - def main(): - """Generate sim_telarray configuration files for a given array.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "layout", "model_version"], + }, + ) array_model = ArrayModel( label=app_context.args["label"], diff --git a/src/simtools/applications/generate_corsika_histograms.py b/src/simtools/applications/generate_corsika_histograms.py index 407fdf5c83..f003048dcb 100644 --- a/src/simtools/applications/generate_corsika_histograms.py +++ b/src/simtools/applications/generate_corsika_histograms.py @@ -69,40 +69,35 @@ from astropy import units as u -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.corsika.corsika_histograms import CorsikaHistograms from simtools.visualization import plot_corsika_histograms -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Generate histograms for the Cherenkov photons saved in the CORSIKA IACT file.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input_files", help="Name(s) of the CORSIKA IACT file(s) to process", type=str, nargs="+", required=True, ) - config.parser.add_argument( + parser.add_argument( "--file_labels", help="Labels for the input files (in the same order as input_files)", type=str, nargs="+", required=None, ) - config.parser.add_argument( + parser.add_argument( "--normalization", help="Normalization method for histograms. Options: 'per-telescope', 'per-bin'", type=str, choices=["per-telescope", "per-bin"], default="per-telescope", ) - config.parser.add_argument( + parser.add_argument( "--axis_distance", help=( "Distance from x/y axes to consider when calculating " @@ -111,18 +106,19 @@ def _parse(): type=float, default=1000.0, ) - config.parser.add_argument( + parser.add_argument( "--pdf_file_name", help="Save histograms into a pdf file.", type=str, required=None, ) - return config.initialize(db_config=False, paths=True) def main(): - """Generate a set of histograms for the Cherenkov photons from CORSIKA IACT file(s).""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "paths": True}, + ) all_histograms = [] for input_file in app_context.args["input_files"]: diff --git a/src/simtools/applications/generate_default_metadata.py b/src/simtools/applications/generate_default_metadata.py index 505fcd48b6..646dcf3716 100644 --- a/src/simtools/applications/generate_default_metadata.py +++ b/src/simtools/applications/generate_default_metadata.py @@ -21,38 +21,32 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model import metadata_model from simtools.io import ascii_handler -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Generate a default simtools metadata file from a json schema.", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--schema", help="schema file describing input data", type=str, required=True, ) - config.parser.add_argument( + parser.add_argument( "--output_file", help="output file name (if not given: print to stdout)", type=str, required=False, ) - return config.initialize(output=False, require_command_line=True) - def main(): - """Generate a default simtools metadata file from a json schema.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"output": False, "require_command_line": True}, + ) default_values = metadata_model.get_default_metadata_dict(app_context.args["schema"]) diff --git a/src/simtools/applications/generate_regular_arrays.py b/src/simtools/applications/generate_regular_arrays.py index ed8e722726..927f663d4d 100644 --- a/src/simtools/applications/generate_regular_arrays.py +++ b/src/simtools/applications/generate_regular_arrays.py @@ -39,49 +39,48 @@ import astropy.units as u import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.layout.array_layout_utils import create_regular_array, write_array_elements_info_yaml -def _parse(): - config = configurator.Configurator( - label=get_application_label(__file__), - description=("Generate a regular array of telescope and save as astropy table."), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--telescope_type", help="Type of telescope (e.g., LST, MST, SST).", type=str, default="LST", ) - config.parser.add_argument( + parser.add_argument( "--n_telescopes", help="Number of telescopes in the array.", type=int, default=4, ) - config.parser.add_argument( + parser.add_argument( "--telescope_distance", help="Distance between telescopes in the array (in meters).", type=float, default=50.0, ) - config.parser.add_argument( + parser.add_argument( "--array_shape", help="Shape of the array (e.g., 'square', 'star').", type=str, default="square", choices=["square", "star"], ) - return config.initialize( - db_config=False, simulation_model=["site", "model_version"], output=True - ) def main(): - """Create layout array files of regular arrays.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": False, + "simulation_model": ["site", "model_version"], + "output": True, + }, + ) n_tel = app_context.args["n_telescopes"] tel_type = app_context.args["telescope_type"] diff --git a/src/simtools/applications/generate_simtel_event_data.py b/src/simtools/applications/generate_simtel_event_data.py index 5d22237883..89f5772078 100644 --- a/src/simtools/applications/generate_simtel_event_data.py +++ b/src/simtools/applications/generate_simtel_event_data.py @@ -126,44 +126,37 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model.metadata_collector import MetadataCollector from simtools.io import io_handler, table_handler from simtools.sim_events.writer import EventDataWriter -def _parse(): - """Parse command line arguments.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Process files and store reduced dataset with event information, " - "array information and triggered telescopes." - ), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input", type=str, required=True, help="Input file path (wildcards allowed; e.g., '/data_path/gamma_*dark*.simtel.zst')", ) - config.parser.add_argument( + parser.add_argument( "--max_files", type=int, default=100, help="Maximum number of input files to process." ) - config.parser.add_argument( + parser.add_argument( "--print_dataset_information", type=int, help="Print data set information for the given number of events.", default=0, ) - return config.initialize(db_config=False, output=True) def main(): - """Generate a reduced dataset of event data from output of telescope simulations.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "output": True}, + startup_kwargs={"setup_io_handler": False}, + ) app_context.logger.info(f"Loading input files from: {app_context.args['input']}") input_pattern = Path(app_context.args["input"]) diff --git a/src/simtools/applications/maintain_simulation_model_add_production.py b/src/simtools/applications/maintain_simulation_model_add_production.py index 1fc52ef826..0eb4f7cd00 100644 --- a/src/simtools/applications/maintain_simulation_model_add_production.py +++ b/src/simtools/applications/maintain_simulation_model_add_production.py @@ -36,23 +36,19 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model import model_repository -def _parse(): - """Parse command line arguments.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Generate a new simulation model production", - ) - return config.initialize(db_config=False, output=False, simulation_model=["model_version"]) - - def main(): - """Generate a new simulation model production.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": False, + "output": False, + "simulation_model": ["model_version"], + }, + ) model_repository.generate_new_production( model_version=app_context.args["model_version"], diff --git a/src/simtools/applications/maintain_simulation_model_compare_productions.py b/src/simtools/applications/maintain_simulation_model_compare_productions.py index 0186625fb4..6faa4c8157 100644 --- a/src/simtools/applications/maintain_simulation_model_compare_productions.py +++ b/src/simtools/applications/maintain_simulation_model_compare_productions.py @@ -16,30 +16,24 @@ from pathlib import Path import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.io import ascii_handler -def _parse(): - """Parse command line arguments.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Compare two directories with model production tables in JSON format.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--directory_1", type=str, required=True, help="Path to the first directory containing JSON files.", ) - config.parser.add_argument( + parser.add_argument( "--directory_2", type=str, required=True, help="Path to the second directory containing JSON files.", ) - return config.initialize(db_config=False, output=False) def _print_differences(differences, rel_path): @@ -86,8 +80,11 @@ def _compare_json_dirs(dir1, dir2, ignore_key="model_version"): def main(): - """Compare two directories with model production tables in JSON format.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "output": False}, + startup_kwargs={"setup_io_handler": False}, + ) _compare_json_dirs(Path(app_context.args["directory_1"]), Path(app_context.args["directory_2"])) diff --git a/src/simtools/applications/maintain_simulation_model_verify_production_tables.py b/src/simtools/applications/maintain_simulation_model_verify_production_tables.py index a483261fa4..7a5d9c3f76 100644 --- a/src/simtools/applications/maintain_simulation_model_verify_production_tables.py +++ b/src/simtools/applications/maintain_simulation_model_verify_production_tables.py @@ -17,33 +17,25 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model import model_repository -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Verify simulation model production tables and model parameters for completeness. " - "This application checks that all model parameters defined in the production tables " - "exist in the simulation models repository." - ), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--simulation_models_path", help="Path to the simulation models repository.", type=str, required=True, ) - return config.initialize(db_config=False, output=False, paths=False) def main(): - """Verify simulation model production tables.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "output": False, "paths": False}, + ) if not model_repository.verify_simulation_model_production_tables( simulation_models_path=app_context.args["simulation_models_path"] diff --git a/src/simtools/applications/maintain_simulation_model_write_array_element_positions.py b/src/simtools/applications/maintain_simulation_model_write_array_element_positions.py index 8ce14ef395..29ff8fc35f 100644 --- a/src/simtools/applications/maintain_simulation_model_write_array_element_positions.py +++ b/src/simtools/applications/maintain_simulation_model_write_array_element_positions.py @@ -43,23 +43,18 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.layout.array_layout_utils import write_array_elements_from_file_to_repository -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Add array element positions to model parameter repository", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input", help="File containing a table of array element positions.", required=False, ) - config.parser.add_argument( + parser.add_argument( "--coordinate_system", help="Coordinate system of array element positions (utm or ground).", default="ground", @@ -68,12 +63,16 @@ def _parse(): choices=["ground", "utm"], ) - return config.initialize(db_config=True, output=True, simulation_model=["parameter_version"]) - def main(): - """Application main.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "output": True, + "simulation_model": ["parameter_version"], + }, + ) write_array_elements_from_file_to_repository( coordinate_system=app_context.args["coordinate_system"], diff --git a/src/simtools/applications/merge_tables.py b/src/simtools/applications/merge_tables.py index 5eddce8dec..b956f90b76 100644 --- a/src/simtools/applications/merge_tables.py +++ b/src/simtools/applications/merge_tables.py @@ -36,41 +36,33 @@ """ -from pathlib import Path - import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator -from simtools.io import io_handler, table_handler - +from simtools.application_control import build_application +from simtools.io import table_handler -def _parse(): - """Parse command line arguments.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Merge tables from multiple input files into single tables.", - ) - input_group = config.parser.add_mutually_exclusive_group(required=True) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + input_group = parser.add_mutually_exclusive_group(required=True) input_group.add_argument( "--input_files", type=str, nargs="+", help="Input file(s) (e.g., 'file1 file2') or a file with a list of input files.", ) - config.parser.add_argument( + parser.add_argument( "--table_names", type=str, nargs="+", help="Names of tables to merge from each input file.", ) - return config.initialize(db_config=False, output=True) - def main(): - """Merge tables from multiple input files into single tables.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "output": True}, + ) app_context.logger.info(f"Loading input files: {app_context.args['input_files']}") @@ -79,8 +71,7 @@ def main(): app_context.args["input_files"], [".hdf5", ".gz"] ) - output_path = io_handler.IOHandler().get_output_directory() - output_filepath = Path(output_path).joinpath(f"{app_context.args['output_file']}") + output_filepath = app_context.io_handler.get_output_file(app_context.args["output_file"]) table_handler.merge_tables( input_files, diff --git a/src/simtools/applications/plot_array_layout.py b/src/simtools/applications/plot_array_layout.py index 6ae9868b0b..f9d8f139ca 100644 --- a/src/simtools/applications/plot_array_layout.py +++ b/src/simtools/applications/plot_array_layout.py @@ -142,44 +142,34 @@ """ import simtools.layout.array_layout_utils as layout_utils -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.visualization.plot_array_layout import plot_array_layouts -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Plots array layout.", - usage=( - "Use '--array_layout_name plot_all' to plot all layouts for the given site " - "and model version." - ), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--figure_name", help="Name of the output figure to be saved into as a pdf.", type=str, required=False, default=None, ) - config.parser.add_argument( + parser.add_argument( "--show_labels", help="Plot array element labels.", action="store_true", required=False, default=False, ) - config.parser.add_argument( + parser.add_argument( "--marker_scaling", help="Scaling factor for the markers.", type=float, required=False, default=1.0, ) - config.parser.add_argument( + parser.add_argument( "--coordinate_system", help="Coordinate system for the array layout.", type=str, @@ -187,14 +177,14 @@ def _parse(): default="ground", choices=["ground", "utm"], ) - config.parser.add_argument( + parser.add_argument( "--axes_range", help="Range of the both axes in meters.", type=float, required=False, default=None, ) - config.parser.add_argument( + parser.add_argument( "--x_lim", help="Explicit x-axis limits [xmin xmax] in meters.", type=float, @@ -203,7 +193,7 @@ def _parse(): default=None, metavar=("XMIN", "XMAX"), ) - config.parser.add_argument( + parser.add_argument( "--y_lim", help="Explicit y-axis limits [ymin ymax] in meters.", type=float, @@ -212,14 +202,14 @@ def _parse(): default=None, metavar=("YMIN", "YMAX"), ) - config.parser.add_argument( + parser.add_argument( "--array_layout_name_background", help="Name of the background layout array (e.g., test_layout, alpha, 4mst, etc.).", type=str, required=False, default=None, ) - config.parser.add_argument( + parser.add_argument( "--grayed_out_array_elements", help="List of array elements to plot as gray circles.", type=str, @@ -227,7 +217,7 @@ def _parse(): required=False, default=None, ) - config.parser.add_argument( + parser.add_argument( "--highlighted_array_elements", help="List of array elements to plot with red circles around them.", type=str, @@ -235,7 +225,7 @@ def _parse(): required=False, default=None, ) - config.parser.add_argument( + parser.add_argument( "--legend_location", help=( "Location of the legend (e.g., 'best', 'upper right', 'upper left', " @@ -246,7 +236,7 @@ def _parse(): required=False, default="best", ) - config.parser.add_argument( + parser.add_argument( "--bounds", help=("Axis bounds mode: 'symmetric' uses +-R with padding, 'exact' uses per-axis min/max"), type=str, @@ -254,29 +244,32 @@ def _parse(): required=False, default="symmetric", ) - config.parser.add_argument( + parser.add_argument( "--padding", help=("Fractional padding applied around computed extents (used for both modes)."), type=float, required=False, default=0.1, ) - return config.initialize( - db_config=True, - simulation_model=[ - "site", - "model_version", - "layout", - "layout_file", - "plot_all_layouts", - "layout_parameter_file", - ], - ) def main(): - """Plot array layout application.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + usage="Use '--array_layout_name plot_all' to plot all layouts for the given site " + "and model version.", + initialization_kwargs={ + "db_config": True, + "simulation_model": [ + "site", + "model_version", + "layout", + "layout_file", + "plot_all_layouts", + "layout_parameter_file", + ], + }, + ) layouts, background_layout = layout_utils.read_layouts(app_context.args) plot_array_layouts( diff --git a/src/simtools/applications/plot_production_grid.py b/src/simtools/applications/plot_production_grid.py index 889ed40118..7a067f957a 100644 --- a/src/simtools/applications/plot_production_grid.py +++ b/src/simtools/applications/plot_production_grid.py @@ -50,28 +50,22 @@ import logging -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.site_model import SiteModel from simtools.production_configuration.plot_production_grid import ProductionGridPlotter logger = logging.getLogger(__name__) -def _parse(): - """Parse command line arguments.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Plot production grid points on sky coordinate projections.", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--grid_points_file", type=str, required=True, help="Path to the ECSV file containing grid points.", ) - config.parser.add_argument( + parser.add_argument( "--observation_time", type=str, default=None, @@ -80,13 +74,13 @@ def _parse(): "If not provided, uses observing time stored in the grid file metadata when present." ), ) - config.parser.add_argument( + parser.add_argument( "--plot_ra_dec_tracks", action="store_true", default=False, help="Plot manual or inferred RA/Dec guide tracks on the sky projection.", ) - config.parser.add_argument( + parser.add_argument( "--dec_values", nargs="+", type=float, @@ -94,16 +88,15 @@ def _parse(): help="Optional list of declination values in degrees to plot as manual tracks.", ) - return config.initialize( - db_config=True, - output=True, - simulation_model=["version", "site", "model_version"], - ) - def main(): """Run the ProductionGridPlotter.""" - app_context = startup_application(_parse) + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["version", "site", "model_version"], + } + ) site_model = SiteModel( model_version=app_context.args["model_version"], site=app_context.args["site"], diff --git a/src/simtools/applications/plot_simtel_events.py b/src/simtools/applications/plot_simtel_events.py index 1282f53c9f..afe3f8aca7 100644 --- a/src/simtools/applications/plot_simtel_events.py +++ b/src/simtools/applications/plot_simtel_events.py @@ -78,103 +78,94 @@ """ import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.visualization.plot_simtel_events import PLOT_CHOICES, generate_and_save_plots -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Create diagnostic plots from sim_telarray files using simtools visualization." - ), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--simtel_file", help="Input sim_telarray file (.simtel.zst)", required=True, ) - config.parser.add_argument( + parser.add_argument( "--plots", help=f"Plots to generate. Choices: {', '.join(sorted(PLOT_CHOICES))}", nargs="+", default=["all"], choices=sorted(PLOT_CHOICES), ) - config.parser.add_argument( + parser.add_argument( "--n_pixels", type=int, default=3, help="For time_traces: number of pixel traces" ) - config.parser.add_argument( + parser.add_argument( "--pixel_step", type=int, default=10, help="Step between pixel ids for step plots" ) - config.parser.add_argument( + parser.add_argument( "--max_pixels", type=int, default=None, help="Cap number of pixels for step traces" ) - config.parser.add_argument("--vmax", type=float, default=None, help="Color scale vmax") - config.parser.add_argument( + parser.add_argument("--vmax", type=float, default=None, help="Color scale vmax") + parser.add_argument( "--half_width", type=int, default=8, help="Half window width for integrated images" ) - config.parser.add_argument( + parser.add_argument( "--offset", type=int, default=16, help="offset between pedestal and peak windows (integrated_pedestal_image)", ) - config.parser.add_argument( + parser.add_argument( "--sum_threshold", type=float, default=10.0, help="Minimum pixel sum to consider in peak timing", ) - config.parser.add_argument( - "--peak_width", type=int, default=8, help="Expected peak width in samples" - ) - config.parser.add_argument( - "--examples", type=int, default=3, help="Number of example traces to draw" - ) - config.parser.add_argument( + parser.add_argument("--peak_width", type=int, default=8, help="Expected peak width in samples") + parser.add_argument("--examples", type=int, default=3, help="Number of example traces to draw") + parser.add_argument( "--timing_bins", type=int, default=None, help="Number of bins for timing histogram (contiguous if not set)", ) - config.parser.add_argument( + parser.add_argument( "--distance", type=float, default=None, help="Optional distance annotation for event_image (same units as input expects)", ) - config.parser.add_argument( + parser.add_argument( "--event_id", type=int, nargs="+", default=None, help="Event ID(s) of the events to be plotted", ) - config.parser.add_argument( + parser.add_argument( "--max_events", type=int, default=1, help="Maximum number of events to process", ) - config.parser.add_argument( + parser.add_argument( "--save_pngs", action="store_true", help="Also save individual PNG images per plot", ) - config.parser.add_argument("--dpi", type=int, default=300, help="PNG dpi") - - return config.initialize( - db_config=False, simulation_model=["telescope"], output=True, require_command_line=True - ) + parser.add_argument("--dpi", type=int, default=300, help="PNG dpi") def main(): - """Generate plots from sim_telarray file.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": False, + "simulation_model": ["telescope"], + "output": True, + "require_command_line": True, + }, + ) plots = list(gen.ensure_iterable(app_context.args.get("plots"))) generate_and_save_plots(plots=plots, args=app_context.args, ioh=app_context.io_handler) diff --git a/src/simtools/applications/plot_simulated_event_distributions.py b/src/simtools/applications/plot_simulated_event_distributions.py index 8500960401..0aa2867874 100644 --- a/src/simtools/applications/plot_simulated_event_distributions.py +++ b/src/simtools/applications/plot_simulated_event_distributions.py @@ -25,25 +25,21 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.sim_events.histograms import EventDataHistograms from simtools.visualization import plot_simtel_event_histograms -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Plot simulated event distributions for shower and/or triggered event data.", - ) - config.parser.add_argument("--input_file", type=str, required=True, help="Input file path") - return config.initialize(db_config=False, output=True) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument("--input_file", type=str, required=True, help="Input file path") def main(): - """Plot simulated event distributions.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "output": True}, + ) app_context.logger.info(f"Loading input file from: {app_context.args['input_file']}") histograms = EventDataHistograms(app_context.args["input_file"]) diff --git a/src/simtools/applications/plot_tabular_data.py b/src/simtools/applications/plot_tabular_data.py index 4faae722d8..ceaa09775a 100644 --- a/src/simtools/applications/plot_tabular_data.py +++ b/src/simtools/applications/plot_tabular_data.py @@ -24,8 +24,7 @@ """ import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.constants import PLOT_CONFIG_SCHEMA from simtools.data_model import schema from simtools.data_model.metadata_collector import MetadataCollector @@ -33,40 +32,36 @@ from simtools.visualization import plot_tables -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Plots tabular data.", - usage="""simtools-plot-tabular-data --plot_config config_file_name " - --output_file output_file_name""", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--plot_config", help="Plotting configuration file name.", type=str, required=True, default=None, ) - config.parser.add_argument( + parser.add_argument( "--table_data_path", help="Path to the data files (optional). Expect all files to be in the same directory.", type=str, default=None, ) - config.parser.add_argument( + parser.add_argument( "--output_file", help="Output file name (without suffix)", type=str, required=True, ) - return config.initialize(db_config=True, simulation_model=["telescope"]) def main(): - """Plot tabular data.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + usage="""simtools-plot-tabular-data --plot_config config_file_name " + --output_file output_file_name""", + initialization_kwargs={"db_config": True, "simulation_model": ["telescope"]}, + ) plot_config = gen.convert_keys_in_dict_to_lowercase( schema.validate_dict_using_schema( diff --git a/src/simtools/applications/plot_tabular_data_for_model_parameter.py b/src/simtools/applications/plot_tabular_data_for_model_parameter.py index 3623d6e2de..3f931d54e8 100644 --- a/src/simtools/applications/plot_tabular_data_for_model_parameter.py +++ b/src/simtools/applications/plot_tabular_data_for_model_parameter.py @@ -46,33 +46,31 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model.metadata_collector import MetadataCollector from simtools.visualization import plot_tables -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Plots tabular data for a model parameter.", - ) - - config.parser.add_argument("--parameter", type=str, required=True, help="Parameter name.") - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument("--parameter", type=str, required=True, help="Parameter name.") + parser.add_argument( "--plot_type", help="Plot type as defined in the schema file.", type=str, required=True, default=None, ) - return config.initialize(db_config=True, simulation_model=["telescope", "parameter_version"]) def main(): - """Plot tabular data.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "parameter_version"], + }, + ) plot_configs, output_files = plot_tables.generate_plot_configurations( parameter=app_context.args["parameter"], diff --git a/src/simtools/applications/production_derive_corsika_limits.py b/src/simtools/applications/production_derive_corsika_limits.py index 17163bb293..b90ff829c3 100644 --- a/src/simtools/applications/production_derive_corsika_limits.py +++ b/src/simtools/applications/production_derive_corsika_limits.py @@ -82,57 +82,48 @@ --output_file corsika_simulation_limits.ecsv """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.production_configuration.derive_corsika_limits import ( generate_corsika_limits_grid, ) -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Derive limits for energy, radial distance, and viewcone.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["telescope_ids"]) + parser.add_argument( "--event_data_file", type=str, required=True, help="Event data file containing reduced event data.", ) - config.parser.add_argument( - "--telescope_ids", - type=str, - required=False, - help="Path to a file containing telescope configurations.", - ) - config.parser.add_argument( + parser.add_argument( "--loss_fraction", type=float, required=True, help="Maximum event-loss fraction for limit computation.", ) - config.parser.add_argument( + parser.add_argument( "--plot_histograms", help="Plot histograms of the event data.", action="store_true", default=False, ) - return config.initialize( - db_config=True, - output=True, - simulation_model=[ - "site", - "model_version", - "layout", - ], - ) def main(): - """Derive limits for energy, radial distance, and viewcone.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "output": True, + "simulation_model": [ + "site", + "model_version", + "layout", + ], + }, + ) generate_corsika_limits_grid(app_context.args) diff --git a/src/simtools/applications/production_derive_statistics.py b/src/simtools/applications/production_derive_statistics.py index ee04f50b06..e25eee3e85 100644 --- a/src/simtools/applications/production_derive_statistics.py +++ b/src/simtools/applications/production_derive_statistics.py @@ -39,7 +39,7 @@ --file_name_template "prod6_LaPalma-{zenith}deg\\ _gamma_cone.N.Am-4LSTs09MSTs_ID0_reduced.fits" \\ --zeniths 20 40 52 60 \\ - --offsets 0 \\ + --off_axis_angles 0 \\ --azimuths 180 \\ --nsb 0.0 \\ --plot_production_statistics @@ -50,77 +50,69 @@ added. """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.production_configuration.derive_production_statistics_handler import ( ProductionStatisticsHandler, ) -def _parse(): - """Parse command line arguments.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Evaluate statistical uncertainties from DL2 MC event files and interpolate results." - ), - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--grid_points_production_file", type=str, required=True, help="Path to the ECSV file containing grid points for a production.", ) - config.parser.add_argument( + parser.add_argument( "--metrics_file", required=True, type=str, default=None, help="Metrics definition file. (default: production_simulation_config_metrics.yml)", ) - config.parser.add_argument( + parser.add_argument( "--base_path", type=str, required=True, help="Path to the DL2 MC event files for interpolation.", ) - config.parser.add_argument( + parser.add_argument( "--file_name_template", required=False, type=str, default=("prod6_LaPalma-{zenith}deg_gamma_cone.N.Am-4LSTs09MSTs_ID0_reduced.fits"), help=("Template for the DL2 MC event file name."), ) - config.parser.add_argument( + parser.add_argument( "--zeniths", required=True, nargs="+", type=float, help="List of zenith angles in deg that describe the supplied DL2 files.", ) - config.parser.add_argument( + parser.add_argument( "--azimuths", required=True, nargs="+", type=float, help="List of azimuth angles in deg that describe the supplied DL2 files.", ) - config.parser.add_argument( + parser.add_argument( "--nsb", required=True, nargs="+", type=float, help="List of nsb values that describe the supplied DL2 files.", ) - config.parser.add_argument( - "--offsets", + parser.add_argument( + "--off_axis_angles", required=True, nargs="+", - type=float, + type=parser.quantity("deg"), help="List of camera offsets in deg that describe the supplied DL2 files.", ) - config.parser.add_argument( + parser.add_argument( "--plot_production_statistics", required=False, action="store_true", @@ -128,12 +120,12 @@ def _parse(): help="Plot production statistics.", ) - return config.initialize(db_config=False, output=True) - def main(): - """Run the ProductionStatisticsHandler.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"db_config": False, "output": True}, + ) manager = ProductionStatisticsHandler( app_context.args, output_path=app_context.io_handler.get_output_directory() diff --git a/src/simtools/applications/production_generate_grid.py b/src/simtools/applications/production_generate_grid.py index e61fc5e825..7331d3f36f 100644 --- a/src/simtools/applications/production_generate_grid.py +++ b/src/simtools/applications/production_generate_grid.py @@ -74,27 +74,21 @@ from astropy.coordinates import EarthLocation from astropy.time import Time -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.io.ascii_handler import collect_data_from_file from simtools.model.site_model import SiteModel from simtools.production_configuration.generate_production_grid import GridGeneration -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Generate a grid of simulation points using flexible axes definitions.", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--axes", type=str, required=True, help="Path to a file defining the grid axes.", ) - config.parser.add_argument( + parser.add_argument( "--coordinate_system", type=str, default="zenith_azimuth", @@ -104,7 +98,7 @@ def _parse(): " location/time and converted to zenith/azimuth for interpolation." ), ) - config.parser.add_argument( + parser.add_argument( "--observing_time", type=str, required=False, @@ -112,13 +106,13 @@ def _parse(): "Observation time in UTC (format: 'YYYY-MM-DD HH:MM:SS'). Used only in 'ra_dec' mode." ), ) - config.parser.add_argument( + parser.add_argument( "--output_file", type=str, default="grid_output.ecsv", help="Output file for the generated grid points (default: 'grid_output.ecsv').", ) - config.parser.add_argument( + parser.add_argument( "--telescope_ids", type=str, nargs="*", @@ -128,14 +122,14 @@ def _parse(): "(e.g. MSTN-15)." ), ) - config.parser.add_argument( + parser.add_argument( "--lookup_table", type=str, required=True, help="Path to the lookup table for simulation limits. " "Table required with varying azimuth and or zenith angle. ", ) - config.parser.add_argument( + parser.add_argument( "--simtel_file", type=str, required=False, @@ -145,8 +139,6 @@ def _parse(): ), ) - return config.initialize(db_config=True, simulation_model=["version", "site", "model_version"]) - def load_axes(file_path: str): """ @@ -169,8 +161,13 @@ def load_axes(file_path: str): def main(): - """Run the Grid Generation application.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["version", "site", "model_version"], + }, + ) output_filepath = app_context.io_handler.get_output_file(app_context.args["output_file"]) diff --git a/src/simtools/applications/production_merge_corsika_limits.py b/src/simtools/applications/production_merge_corsika_limits.py index 712dc1e3cd..a003e9cbc5 100644 --- a/src/simtools/applications/production_merge_corsika_limits.py +++ b/src/simtools/applications/production_merge_corsika_limits.py @@ -79,20 +79,15 @@ from pathlib import Path -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model import data_reader from simtools.io import ascii_handler from simtools.production_configuration.merge_corsika_limits import CorsikaMergeLimits -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Merge CORSIKA limit tables and check grid completeness.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input_files", type=str, default=None, @@ -102,7 +97,7 @@ def _parse(): "containing the files (*.ecsv)." ), ) - config.parser.add_argument( + parser.add_argument( "--input_files_list", type=str, default=None, @@ -111,36 +106,37 @@ def _parse(): "to be merged." ), ) - config.parser.add_argument( + parser.add_argument( "--merged_table", type=str, default=None, help="Path to an already merged table file.", ) - config.parser.add_argument( + parser.add_argument( "--grid_definition", type=str, default=None, help="Path to YAML file defining the expected grid points.", ) - config.parser.add_argument( + parser.add_argument( "--plot_grid_coverage", help="Generate plots showing grid coverage.", action="store_true", default=False, ) - config.parser.add_argument( + parser.add_argument( "--plot_limits", help="Generate plots showing the derived limits.", action="store_true", default=False, ) - return config.initialize(output=True) def main(): - """Merge CORSIKA limit tables and check grid completeness.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"output": True}, + ) merger = CorsikaMergeLimits() grid_definition = ( diff --git a/src/simtools/applications/run_application.py b/src/simtools/applications/run_application.py index 32f7160d40..3023b1d70a 100644 --- a/src/simtools/applications/run_application.py +++ b/src/simtools/applications/run_application.py @@ -51,58 +51,54 @@ .. code-block:: console - simtools-run-application --configuration_file config_file_name + simtools-run-application --config_file config_file_name Run the application with the configuration file ``config_file_name``, but skipping all steps except step 2 and 3 (useful for debugging): .. code-block:: console - simtools-run-application --configuration_file config_file_name --steps 2 3 + simtools-run-application --config_file config_file_name --steps 2 3 """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.runners import simtools_runner -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Run simtools applications using a configuration file.", - usage="simtools-run-application --config_file config_file_name", - ) - - config.parser.add_argument( - "--configuration_file", +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( + "--config_file", + dest="configuration_file", help="Application configuration.", type=str, required=True, default=None, ) - config.parser.add_argument( + parser.add_argument( "--steps", type=int, nargs="+", help="List of steps to be execution (e.g., '--steps 7 8 9'; do not specify to run all).", ) - config.parser.add_argument( + parser.add_argument( "--ignore_runtime_environment", action="store_true", help="Ignore the runtime environment and run the application in the current environment.", default=False, ) - return config.initialize(db_config=True) def main(): """Run several simtools applications using a configuration file.""" - app_context = startup_application( - _parse, - setup_io_handler=False, - resolve_sim_software_executables=False, + app_context = build_application( + usage="simtools-run-application --config_file config_file_name", + initialization_kwargs={"db_config": True}, + startup_kwargs={ + "setup_io_handler": False, + "resolve_sim_software_executables": False, + }, ) simtools_runner.run_applications(app_context.args, app_context.logger) diff --git a/src/simtools/applications/simulate_flasher.py b/src/simtools/applications/simulate_flasher.py index ff71bc04d9..b7d87ba63b 100644 --- a/src/simtools/applications/simulate_flasher.py +++ b/src/simtools/applications/simulate_flasher.py @@ -77,20 +77,16 @@ Run number to use (default: 1, required for direct injection mode). """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.model_utils import get_array_elements_for_layout from simtools.simtel.simulator_light_emission import SimulatorLightEmission from simtools.simulator import Simulator from simtools.utils import general -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), description="Simulate flasher devices." - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--run_mode", help="Flasher simulation run mode", type=str, @@ -98,7 +94,7 @@ def _parse(): required=True, default="direct_injection", ) - group = config.parser.add_mutually_exclusive_group(required=True) + group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--light_source", help="Flasher device associated with a specific telescope, i.e. MSFx-FlashCam", @@ -109,11 +105,11 @@ def _parse(): help="Type of the light source (e.g. flat_fielding)", type=str, ) - target_group = config.parser.add_mutually_exclusive_group(required=True) + target_group = parser.add_mutually_exclusive_group(required=True) target_group.add_argument( "--telescopes", help="One or more telescopes (e.g. LSTN-01, MSTN-04, SSTS-04)", - type=config.parser.telescope, + type=parser.telescope, nargs="+", ) target_group.add_argument( @@ -122,7 +118,7 @@ def _parse(): nargs="+", type=str, ) - config.parser.add_argument( + parser.add_argument( "--number_of_events", help="Number of flasher events to simulate", type=int, @@ -130,29 +126,30 @@ def _parse(): nargs="+", required=False, ) - config.parser.add_argument( + parser.add_argument( "--flasher_photons", help=( "Override flasher photon yield (single value for all telescopes). " "Accepts integers including scientific notation, e.g. 1e6." ), - type=config.parser.scientific_int, + type=parser.scientific_int, nargs="+", required=False, ) - return config.initialize( - db_config=True, - simulation_model=["site", "model_version"], - simulation_configuration={ - "corsika_configuration": ["run_number"], - "sim_telarray_configuration": ["all"], - }, - ) def main(): - """Simulate flasher devices.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "model_version"], + "simulation_configuration": { + "corsika_configuration": ["run_number"], + "sim_telarray_configuration": ["all"], + }, + }, + ) tel_string = ( f"telescope(s) {app_context.args['telescopes']}" diff --git a/src/simtools/applications/simulate_illuminator.py b/src/simtools/applications/simulate_illuminator.py index 4629eec32b..bba3b08ee8 100644 --- a/src/simtools/applications/simulate_illuminator.py +++ b/src/simtools/applications/simulate_illuminator.py @@ -61,27 +61,20 @@ Light source pointing direction. If not set, the pointing from the simulation model is used. """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.simtel.simulator_light_emission import SimulatorLightEmission -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Simulate light emission by a calibration light source (not attached to a telescope)." - ), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--light_source", help="Illuminator name, i.e. ILLN-design", type=str, default=None, required=True, ) - configurable_light_source_args = config.parser.add_argument_group( + configurable_light_source_args = parser.add_argument_group( "Configurable light source position and pointing (override simulation model values)" ) configurable_light_source_args.add_argument( @@ -101,32 +94,33 @@ def _parse(): nargs=3, required=False, ) - config.parser.add_argument( + parser.add_argument( "--number_of_events", help="Number of events to simulate", type=int, default=1, required=False, ) - config.parser.add_argument( + parser.add_argument( "--flasher_photons", help=( "Override flasher photon yield. " "Accepts integers including scientific notation, e.g. 1e8." ), - type=config.parser.scientific_int, + type=parser.scientific_int, required=False, ) - return config.initialize( - db_config=True, - simulation_model=["telescope", "model_version"], - require_command_line=True, - ) def main(): - """Simulate light emission from illuminator.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "model_version"], + "require_command_line": True, + }, + ) light_source = SimulatorLightEmission( light_emission_config={**app_context.args, "run_mode": "illuminator"}, diff --git a/src/simtools/applications/simulate_pedestals.py b/src/simtools/applications/simulate_pedestals.py index 9cccdf3c43..08525d6ee6 100644 --- a/src/simtools/applications/simulate_pedestals.py +++ b/src/simtools/applications/simulate_pedestals.py @@ -54,30 +54,26 @@ Azimuth angle in degrees. """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.simulator import Simulator -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), description="Simulate calibration events." - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--run_mode", help="Calibration run mode", type=str, required=True, choices=["pedestals", "pedestals_dark", "pedestals_nsb_only"], ) - config.parser.add_argument( + parser.add_argument( "--number_of_events", help="Number of pedestal events to simulate", type=int, required=True, ) - config.parser.add_argument( + parser.add_argument( "--nsb_scaling_factor", help=( "Scaling factor for the NSB rate. " @@ -87,26 +83,26 @@ def _parse(): required=False, default=1.0, ) - config.parser.add_argument( + parser.add_argument( "--stars", help="List of stars (azimuth, zenith, weighting factor).", type=str, default=None, ) - return config.initialize( - db_config=True, - simulation_model=["site", "layout", "telescope", "model_version"], - simulation_configuration={ - "corsika_configuration": ["run_number", "azimuth_angle", "zenith_angle"], - "sim_telarray_configuration": ["all"], - }, - ) - def main(): - """Simulate pedestal events.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "layout", "telescope", "model_version"], + "simulation_configuration": { + "corsika_configuration": ["run_number", "azimuth_angle", "zenith_angle"], + "sim_telarray_configuration": ["all"], + }, + }, + ) simulator = Simulator(label=app_context.args.get("label")) simulator.simulate() diff --git a/src/simtools/applications/simulate_prod.py b/src/simtools/applications/simulate_prod.py index 8e4e25bafd..a1b6e9bb1d 100644 --- a/src/simtools/applications/simulate_prod.py +++ b/src/simtools/applications/simulate_prod.py @@ -56,18 +56,15 @@ --zenith_angle 20 --start_run 0 --run 1 """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import commandline_parser, configurator +from simtools.application_control import build_application +from simtools.configuration import commandline_parser from simtools.constants import CORSIKA_MAX_SEED from simtools.simulator import Simulator -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), description="Run simulations for productions" - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--corsika_file", help=( "Path to the CORSIKA input file (only relevant for simulation software 'sim_telarray')." @@ -75,21 +72,21 @@ def _parse(): type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--pack_for_grid_register", help="Directory for a tarball for registering the output files on the grid.", type=str, required=False, default=None, ) - config.parser.add_argument( + parser.add_argument( "--save_file_lists", help="Save lists of output and log files.", action="store_true", required=False, default=False, ) - config.parser.add_argument( + parser.add_argument( "--save_reduced_event_lists", help=( "Save reduced event lists with event data on simulated and triggered events. " @@ -99,14 +96,14 @@ def _parse(): required=False, default=False, ) - config.parser.add_argument( + parser.add_argument( "--corsika_seeds", help="Use fixed random seeds for CORSIKA for testing purposes.", nargs=4, type=commandline_parser.CommandLineParser.bounded_int(1, CORSIKA_MAX_SEED), metavar=("S1", "S2", "S3", "S4"), ) - config.parser.add_argument( + parser.add_argument( "--sequential", help=( "Enables single-core mode (as far as possible); " @@ -115,20 +112,22 @@ def _parse(): action="store_true", default=False, ) - return config.initialize( - db_config=True, - simulation_model=["site", "layout", "telescope", "model_version"], - simulation_configuration={ - "software": None, - "corsika_configuration": ["all"], - "sim_telarray_configuration": ["all"], - }, - ) def main(): - """Run simulations for productions.""" - app_context = startup_application(_parse, setup_io_handler=False) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["site", "layout", "telescope", "model_version"], + "simulation_configuration": { + "software": None, + "corsika_configuration": ["all"], + "sim_telarray_configuration": ["all"], + }, + }, + startup_kwargs={"setup_io_handler": False}, + ) simulator = Simulator(label=app_context.args.get("label")) diff --git a/src/simtools/applications/simulate_prod_htcondor_generator.py b/src/simtools/applications/simulate_prod_htcondor_generator.py index 5bcd33fc83..8feb04720e 100644 --- a/src/simtools/applications/simulate_prod_htcondor_generator.py +++ b/src/simtools/applications/simulate_prod_htcondor_generator.py @@ -45,47 +45,43 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.job_execution import htcondor_script_generator -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Prepare simulations production for HT Condor job submission", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--number_of_runs", help="Number of runs to be simulated.", type=int, required=True, default=1, ) - config.parser.add_argument( + parser.add_argument( "--apptainer_image", help="Apptainer image to use for the simulation (full path).", type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--priority", help="Job priority.", type=int, required=False, default=1, ) - return config.initialize( - db_config=False, - simulation_model=["site", "layout", "telescope", "model_version"], - simulation_configuration={"software": None, "corsika_configuration": ["all"]}, - ) def main(): - """Generate HT Condor submission script and submit file.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": False, + "simulation_model": ["site", "layout", "telescope", "model_version"], + "simulation_configuration": {"software": None, "corsika_configuration": ["all"]}, + }, + ) htcondor_script_generator.generate_submission_script(app_context.args) diff --git a/src/simtools/applications/submit_array_layouts.py b/src/simtools/applications/submit_array_layouts.py index e1e60d3864..8e1163f2e2 100644 --- a/src/simtools/applications/submit_array_layouts.py +++ b/src/simtools/applications/submit_array_layouts.py @@ -34,33 +34,27 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.db import db_handler from simtools.io import ascii_handler from simtools.layout.array_layout_utils import validate_array_layouts_with_db, write_array_layouts -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Submit and validate array layouts.", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--array_layouts", type=str, required=True, help="Array layout dictionary file.", ) - config.parser.add_argument( + parser.add_argument( "--updated_parameter_version", help="Updated parameter version.", type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--input_meta", help="meta data file(s) associated to input data (wildcards or list of files allowed)", type=str, @@ -68,12 +62,16 @@ def _parse(): required=False, ) - return config.initialize(output=True, db_config=True, simulation_model=["model_version"]) - def main(): - """Submit and validate array layouts.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "output": True, + "db_config": True, + "simulation_model": ["model_version"], + }, + ) db = db_handler.DatabaseHandler() diff --git a/src/simtools/applications/submit_data_from_external.py b/src/simtools/applications/submit_data_from_external.py index ca469e517b..42ff0980f0 100644 --- a/src/simtools/applications/submit_data_from_external.py +++ b/src/simtools/applications/submit_data_from_external.py @@ -37,49 +37,42 @@ """ import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.data_model import validate_data from simtools.data_model.metadata_collector import MetadataCollector -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Submit and validate data (e.g., input data to tools, model parameters).", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--input_meta", help="meta data file associated to input data", type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--input", help="input data file", type=str, required=True, ) - config.parser.add_argument( + parser.add_argument( "--schema", help="schema file describing input data", type=str, required=False, ) - config.parser.add_argument( + parser.add_argument( "--ignore_metadata", help="Ignore metadata", action="store_true", required=False, ) - return config.initialize(output=True) def main(): - """Submit and validate data (e.g., input data to tools, model parameters).""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application(initialization_kwargs={"output": True}) _metadata = ( None if app_context.args.get("ignore_metadata") else MetadataCollector(app_context.args) diff --git a/src/simtools/applications/submit_model_parameter_from_external.py b/src/simtools/applications/submit_model_parameter_from_external.py index 0e4579d3ee..1e12cee28a 100644 --- a/src/simtools/applications/submit_model_parameter_from_external.py +++ b/src/simtools/applications/submit_model_parameter_from_external.py @@ -43,33 +43,25 @@ from pathlib import Path import simtools.data_model.model_data_writer as writer -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.simtel import simtel_table_reader -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Submit and validate a model parameter.", - ) - - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--parameter", type=str, required=True, help="Parameter for simulation model" ) - config.parser.add_argument("--instrument", type=str, required=True, help="Instrument name") - config.parser.add_argument("--site", type=str, required=True, help="Site location") - config.parser.add_argument( - "--parameter_version", type=str, required=True, help="Parameter version" - ) - config.parser.add_argument( + parser.add_argument("--instrument", type=str, required=True, help="Instrument name") + parser.add_argument("--site", type=str, required=True, help="Site location") + parser.add_argument("--parameter_version", type=str, required=True, help="Parameter version") + parser.add_argument( "--model_parameter_schema_version", type=str, required=False, help="Model-parameter schema version to use for validation and value interpretation", ) - config.parser.add_argument( + parser.add_argument( "--value", type=str, required=True, @@ -79,24 +71,25 @@ def _parse(): 'Examples: "--value=5", "--value=\'5 km\'", "--value=\'5 cm, 0.5 deg\'"' ), ) - config.parser.add_argument( + parser.add_argument( "--input_meta", help="meta data file(s) associated to input data (wildcards or list of files allowed)", type=str, nargs="+", required=False, ) - config.parser.add_argument( + parser.add_argument( "--check_parameter_version", help="Check if the parameter version exists in the database", action="store_true", ) - return config.initialize(output=True, db_config=True) def main(): - """Submit and validate a model parameter value and metadata.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"output": True, "db_config": True}, + ) model_parameter_schema_version = app_context.args.get("model_parameter_schema_version") value = app_context.args["value"] data_writer = writer.ModelDataWriter() diff --git a/src/simtools/applications/validate_camera_efficiency.py b/src/simtools/applications/validate_camera_efficiency.py index e6fb905541..6d4af65955 100644 --- a/src/simtools/applications/validate_camera_efficiency.py +++ b/src/simtools/applications/validate_camera_efficiency.py @@ -44,23 +44,15 @@ The output is saved in simtools-output/validate_camera_efficiency. """ -from simtools.application_control import get_application_label, startup_application +from simtools.application_control import build_application from simtools.camera.camera_efficiency import CameraEfficiency -from simtools.configuration import configurator from simtools.io.ascii_handler import write_data_to_file from simtools.utils import names -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Calculate the camera efficiency and NSB pixel rates. " - "Plot the camera efficiency vs wavelength for Cherenkov and NSB light." - ), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--nsb_spectrum", help=( "File with NSB spectrum to use for the efficiency simulation." @@ -73,7 +65,7 @@ def _parse(): default=None, required=False, ) - config.parser.add_argument( + parser.add_argument( "--skip_correction_to_nsb_spectrum", help=( "Skip correction to the NSB spectrum to account for the " @@ -83,27 +75,32 @@ def _parse(): required=False, action="store_true", ) - config.parser.add_argument( + parser.add_argument( "--write_reference_nsb_rate_as_parameter", help=("Write the NSB pixel rate obtained for reference conditions as a model parameter "), action="store_true", required=False, ) - args_dict, db_config = config.initialize( - db_config=True, - simulation_model=["telescope", "model_version", "parameter_version"], - simulation_configuration={"corsika_configuration": ["zenith_angle", "azimuth_angle"]}, - ) + + +def _validate_required_args(args_dict): + """Validate required arguments that must be explicitly provided.""" if args_dict["site"] is None or args_dict["telescope"] is None: - config.parser.print_help() - print("\n\nSite and telescope must be provided\n\n") raise RuntimeError("Site and telescope must be provided") - return args_dict, db_config def main(): - """Calculate the camera efficiency and NSB pixel rates.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "model_version", "parameter_version"], + "simulation_configuration": { + "corsika_configuration": ["zenith_angle", "azimuth_angle"] + }, + }, + ) + _validate_required_args(app_context.args) results = {} for efficiency_type in ["Shower", "NSB", "Muon"]: diff --git a/src/simtools/applications/validate_camera_fov.py b/src/simtools/applications/validate_camera_fov.py index b53a13351e..1f55362f4c 100644 --- a/src/simtools/applications/validate_camera_fov.py +++ b/src/simtools/applications/validate_camera_fov.py @@ -47,22 +47,14 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.telescope_model import TelescopeModel from simtools.visualization import plot_camera, visualize -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Calculate the camera FoV of the telescope requested. " - "Plot the camera, as seen for an observer facing the camera." - ), - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--camera_in_sky_coor", help=( "Plot the camera layout in sky coordinates " @@ -71,7 +63,7 @@ def _parse(): action="store_true", default=False, ) - config.parser.add_argument( + parser.add_argument( "--print_pixels_id", help=( "Up to which pixel ID to print. " @@ -80,12 +72,16 @@ def _parse(): ), default=50, ) - return config.initialize(db_config=True, simulation_model=["telescope", "model_version"]) def main(): - """Validate camera field of view.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "model_version"], + }, + ) label = "validate_camera_fov" diff --git a/src/simtools/applications/validate_cumulative_psf.py b/src/simtools/applications/validate_cumulative_psf.py index f2bb92b7c6..424b09d728 100644 --- a/src/simtools/applications/validate_cumulative_psf.py +++ b/src/simtools/applications/validate_cumulative_psf.py @@ -29,9 +29,9 @@ Telescope model name (e.g. LST-1, SST-D, ...). model_version (str, optional) Model version. - src_distance (float, optional) + source_distance (float or quantity, optional) Source distance in km. - zenith (float, optional) + zenith_angle (float or quantity, optional) Zenith angle in deg. data (str, optional) Name of the data file with the measured cumulative PSF. @@ -76,40 +76,15 @@ import numpy as np import simtools.utils.general as gen -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.model_utils import initialize_simulation_models from simtools.ray_tracing.ray_tracing import RayTracing from simtools.visualization import visualize -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Calculate and plot the PSF and eff. mirror area as a function of off-axis angle " - "of the telescope requested." - ), - ) - config.parser.add_argument( - "--src_distance", - help="Source distance in km", - type=float, - default=10, - ) - config.parser.add_argument( - "--zenith", - help="Zenith angle in deg", - type=float, - default=20.0, - ) - config.parser.add_argument( - "--data", - help="Data file name with the measured PSF vs radius [cm]", - type=str, - ) - return config.initialize(db_config=True, simulation_model=["telescope", "model_version"]) +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments(["source_distance", "zenith_angle", "data"]) def load_data(datafile): @@ -125,8 +100,13 @@ def load_data(datafile): def main(): - """Validate the cumulative PSF of a telescope model against data.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "model_version"], + }, + ) tel_model, site_model, _ = initialize_simulation_models( label=app_context.args.get("label"), @@ -139,8 +119,8 @@ def main(): telescope_model=tel_model, site_model=site_model, label=app_context.args.get("label"), - zenith_angle=app_context.args["zenith"] * u.deg, - source_distance=app_context.args["src_distance"] * u.km, + zenith_angle=app_context.args["zenith_angle"], + source_distance=app_context.args["source_distance"], off_axis_angle=[0.0] * u.deg, ) diff --git a/src/simtools/applications/validate_file_using_schema.py b/src/simtools/applications/validate_file_using_schema.py index bd9dc45fae..45f28fce05 100644 --- a/src/simtools/applications/validate_file_using_schema.py +++ b/src/simtools/applications/validate_file_using_schema.py @@ -48,24 +48,19 @@ """ -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.constants import MODEL_PARAMETER_SCHEMA_PATH from simtools.data_model import metadata_collector, schema, validate_data -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description="Validate a file (metadata, schema, or data file) using a schema.", - ) - config.parser.add_argument( +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.add_argument( "--file_name", help="File to be validated (full path or name pattern, e.g., '*.json')", default="*.json", ) - config.parser.add_argument( + parser.add_argument( "--file_directory", help=( "Directory with files to be validated. " @@ -74,29 +69,30 @@ def _parse(): f"{MODEL_PARAMETER_SCHEMA_PATH}." ), ) - config.parser.add_argument("--schema", help="Schema file", required=False) - config.parser.add_argument( + parser.add_argument("--schema", help="Schema file", required=False) + parser.add_argument( "--data_type", help="Type of input data", choices=["metadata", "schema", "data", "model_parameter"], default="data", ) - config.parser.add_argument( + parser.add_argument( "--check_exact_data_type", help="Require exact data type for validation", action="store_true", ) - config.parser.add_argument( + parser.add_argument( "--ignore_software_version", help="Ignore software version check.", action="store_true", ) - return config.initialize(paths=False) def main(): - """Validate a file or files in a directory using a schema.""" - app_context = startup_application(_parse) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={"paths": False}, + ) file_name = app_context.args.get("file_name") file_directory = app_context.args.get("file_directory") diff --git a/src/simtools/applications/validate_optics.py b/src/simtools/applications/validate_optics.py index 54c811a350..2825b07265 100644 --- a/src/simtools/applications/validate_optics.py +++ b/src/simtools/applications/validate_optics.py @@ -32,13 +32,13 @@ Telescope model name (e.g. LST-1, SST-D, ...). model_version (str, optional) Model version. - src_distance (float, optional) + source_distance (float or quantity, optional) Source distance in km. - zenith (float, optional) + zenith_angle (float or quantity, optional) Zenith angle in deg. - max_offset (float, optional) + max_offset (float or quantity, optional) Maximum offset angle in deg. - offset_steps (float, optional) + offset_step (float or quantity, optional) Offset angle step size. plot_images (activation mode, optional) Produce a multiple pages pdf file with the image plots. @@ -52,7 +52,7 @@ .. code-block:: console simtools-validate-optics --site North --telescope LST-1 --max_offset 1.0 \\ - --zenith 20 --src_distance 10 --test + --zenith_angle 20 --source_distance 10 --test The output is saved in simtools-output/validate_optics @@ -72,67 +72,68 @@ import numpy as np from matplotlib.backends.backend_pdf import PdfPages -from simtools.application_control import get_application_label, startup_application -from simtools.configuration import configurator +from simtools.application_control import build_application from simtools.model.model_utils import initialize_simulation_models from simtools.ray_tracing.ray_tracing import RayTracing from simtools.visualization import visualize -def _parse(): - """Parse command line configuration.""" - config = configurator.Configurator( - label=get_application_label(__file__), - description=( - "Calculate and plot the PSF and effective mirror area as a function of off-axis angle " - "of the telescope requested." - ), +def _add_arguments(parser): + """Register application-specific command line arguments.""" + parser.initialize_application_arguments( + ["source_distance", "zenith_angle", "max_offset", "offset_step"] ) - - config.parser.add_argument( - "--src_distance", - help="Source distance in km", - type=float, - default=10, - ) - config.parser.add_argument("--zenith", help="Zenith angle in deg", type=float, default=20) - config.parser.add_argument( - "--max_offset", - help="Maximum offset angle in deg", - type=float, - default=4, - ) - config.parser.add_argument( - "--offset_steps", - help="Offset angle step size", - type=float, - default=0.25, - ) - config.parser.add_argument( + parser.add_argument( "--offset_file", help="Path to ECSV file with x, y offset columns (in degrees). " - "If provided, overrides max_offset and offset_steps.", + "If provided, overrides max_offset and offset_step.", type=str, default=None, ) - config.parser.add_argument( + parser.add_argument( "--offset_directions", help="Cardinal directions for offset generation (comma-separated): N,S,E,W. " "Only used with max_offset. Default: all four directions.", type=str, default="N,S,E,W", ) - config.parser.add_argument( + parser.add_argument( "--plot_images", help="Produce a multiple pages pdf file with the image plots.", action="store_true", ) - return config.initialize(db_config=True, simulation_model=["telescope", "model_version"]) + + +def _validate_offset_parameters(max_offset, offset_step): + """ + Validate offset sampling parameters before calling np.linspace. + + Parameters + ---------- + max_offset : float + Maximum off-axis angle in degrees. + offset_step : float + Step size between off-axis angles in degrees. + + Raises + ------ + ValueError + If offset_step is not positive or max_offset is negative. + """ + if offset_step <= 0: + raise ValueError(f"offset_step must be positive, got {offset_step} deg.") + if max_offset < 0: + raise ValueError(f"max_offset must be non-negative, got {max_offset} deg.") def main(): - """Validate the optical model parameters through ray tracing simulations.""" - app_context = startup_application(_parse, setup_io_handler=True) + """See CLI description.""" + app_context = build_application( + initialization_kwargs={ + "db_config": True, + "simulation_model": ["telescope", "model_version"], + }, + ) tel_model, site_model, _ = initialize_simulation_models( label=Path(__file__).stem, @@ -151,16 +152,21 @@ def main(): d.strip().upper() for d in app_context.args["offset_directions"].split(",") ] + max_offset = app_context.args["max_offset"].to_value(u.deg) + offset_step = app_context.args["offset_step"].to_value(u.deg) + + _validate_offset_parameters(max_offset, offset_step) + ray = RayTracing( telescope_model=tel_model, site_model=site_model, label=app_context.args.get("label") or Path(__file__).stem, - zenith_angle=app_context.args["zenith"] * u.deg, - source_distance=app_context.args["src_distance"] * u.km, + zenith_angle=app_context.args["zenith_angle"], + source_distance=app_context.args["source_distance"], off_axis_angle=np.linspace( 0, - app_context.args["max_offset"], - int(app_context.args["max_offset"] / app_context.args["offset_steps"]) + 1, + max_offset, + int(max_offset / offset_step) + 1, ) * u.deg, offset_file=app_context.args.get("offset_file"), diff --git a/src/simtools/configuration/commandline_parser.py b/src/simtools/configuration/commandline_parser.py index c4acf4b8d2..7f9367c0f5 100644 --- a/src/simtools/configuration/commandline_parser.py +++ b/src/simtools/configuration/commandline_parser.py @@ -72,14 +72,12 @@ def initialize_config_files(self): help="simtools configuration file", default=None, type=str, - required=False, ) _job_group.add_argument( "--env_file", help="file with environment variables", default=".env", type=str, - required=False, ) def initialize_path_arguments(self): @@ -90,27 +88,23 @@ def initialize_path_arguments(self): help="path pointing towards data directory", type=Path, default="./data/", - required=False, ) _job_group.add_argument( "--output_path", help="path pointing towards output directory", type=Path, default="./simtools-output/", - required=False, ) _job_group.add_argument( "--model_path", help="path pointing towards simulation model file directory", type=Path, default="./", - required=False, ) _job_group.add_argument( "--sim_telarray_path", help="path pointing to sim_telarray installation", type=Path, - required=False, ) def initialize_output_arguments(self): @@ -120,20 +114,16 @@ def initialize_output_arguments(self): "--output_file", help="output data file", type=str, - required=False, ) _job_group.add_argument( "--output_file_format", help="file format of output data", type=str, default="ecsv", - required=False, ) _job_group.add_argument( "--skip_output_validation", help="skip output data validation against schema", - default=False, - required=False, action="store_true", ) @@ -144,24 +134,20 @@ def initialize_application_execution_arguments(self): "--test", help="test option for faster execution during development", action="store_true", - required=False, ) _job_group.add_argument( "--label", help="job label", - required=False, ) _job_group.add_argument( "--log_level", action="store", default="info", help="log level to print", - required=False, ) _job_group.add_argument( "--log_file", help="log file path", - required=False, type=Path, ) _job_group.add_argument( @@ -176,68 +162,33 @@ def initialize_application_execution_arguments(self): _job_group.add_argument( "--export_build_info", help="export build information to file", - required=False, type=str, ) def initialize_user_arguments(self): """Initialize user arguments.""" _job_group = self.add_argument_group("user") - _job_group.add_argument( - "--user_name", - help="user name", - type=str, - required=False, - ) - _job_group.add_argument( - "--user_organization", - help="user organization", - type=str, - required=False, - ) - _job_group.add_argument( - "--user_email", - help="user email", - type=str, - required=False, - ) - _job_group.add_argument( - "--user_orcid", - help="user ORCID", - type=str, - required=False, - ) + for flag, help_text in [ + ("user_name", "user name"), + ("user_organization", "user organization"), + ("user_email", "user email"), + ("user_orcid", "user ORCID"), + ]: + _job_group.add_argument(f"--{flag}", help=help_text, type=str) def initialize_db_config_arguments(self): """Initialize DB configuration parameters.""" _job_group = self.add_argument_group("database configuration") - _job_group.add_argument("--db_api_user", help="database user", type=str, required=False) - _job_group.add_argument("--db_api_pw", help="database password", type=str, required=False) - _job_group.add_argument("--db_api_port", help="database port", type=int, required=False) - _job_group.add_argument( - "--db_server", help="database server address", type=str, required=False - ) - _job_group.add_argument( - "--db_api_authentication_database", - help="database with user info (optional)", - type=str, - required=False, - default=None, - ) - _job_group.add_argument( - "--db_simulation_model", - help="name of simulation model database", - type=str.strip, - required=False, - default=None, - ) - _job_group.add_argument( - "--db_simulation_model_version", - help="version of simulation model database", - type=str.strip, - required=False, - default=None, - ) + for flag, help_text, arg_type in [ + ("db_api_user", "database user", str), + ("db_api_pw", "database password", str), + ("db_api_port", "database port", int), + ("db_server", "database server address", str), + ("db_api_authentication_database", "database with user info (optional)", str), + ("db_simulation_model", "name of simulation model database", str.strip), + ("db_simulation_model_version", "version of simulation model database", str.strip), + ]: + _job_group.add_argument(f"--{flag}", help=help_text, type=arg_type) def initialize_simulation_model_arguments(self, model_options): """ @@ -273,7 +224,6 @@ def initialize_simulation_model_arguments(self, model_options): "--overwrite_model_parameters", help="File name to overwrite model parameters from DB with provided values", type=str, - required=False, ) if any( @@ -308,7 +258,6 @@ def initialize_simulation_model_arguments(self, model_options): "--ignore_missing_design_model", help="Ignore missing design model definition of DB", action="store_true", - required=False, ) def initialize_simulation_configuration_arguments(self, simulation_configuration): @@ -324,36 +273,48 @@ def initialize_simulation_configuration_arguments(self, simulation_configuration return if "software" in simulation_configuration: - self._initialize_simulation_software() + _grp = self.add_argument_group("simulation software") + _grp.add_argument( + "--simulation_software", + help="Simulation software steps.", + type=str, + choices=["corsika", "sim_telarray", "corsika_sim_telarray"], + required=True, + default="corsika_sim_telarray", + ) if "corsika_configuration" in simulation_configuration: - self._initialize_simulation_configuration( - group_name="simulation configuration", - selected_parameters=simulation_configuration["corsika_configuration"], - available_parameters=self._get_dictionary_with_corsika_configuration(), + self._initialize_argument_group( + "simulation configuration", + simulation_configuration["corsika_configuration"], + self._get_dictionary_with_corsika_configuration(), ) - self._initialize_simulation_configuration( - group_name="shower parameters", - selected_parameters=simulation_configuration["corsika_configuration"], - available_parameters=self._get_dictionary_with_shower_configuration(), + self._initialize_argument_group( + "shower parameters", + simulation_configuration["corsika_configuration"], + _SHOWER_ARGS, ) if "sim_telarray_configuration" in simulation_configuration: - self._initialize_simulation_configuration( - group_name="sim_telarray configuration", - selected_parameters=simulation_configuration["sim_telarray_configuration"], - available_parameters=self._get_dictionary_with_sim_telarray_configuration(), + self._initialize_argument_group( + "sim_telarray configuration", + simulation_configuration["sim_telarray_configuration"], + _SIMTEL_ARGS, ) - def _initialize_simulation_software(self): - """Initialize simulation software arguments.""" - _software_group = self.add_argument_group("simulation software") - _software_group.add_argument( - "--simulation_software", - help="Simulation software steps.", - type=str, - choices=["corsika", "sim_telarray", "corsika_sim_telarray"], - required=True, - default="corsika_sim_telarray", - ) + def initialize_application_arguments(self, selected_parameters, group_name="application"): + """ + Initialize reusable application-specific arguments. + + Parameters + ---------- + selected_parameters : list + List of application argument names to initialize. + group_name : str, optional + Name of the argument group. + """ + if selected_parameters is None: + return + + self._initialize_argument_group(group_name, selected_parameters, _APPLICATION_ARGS) @staticmethod def _get_dictionary_with_corsika_configuration(): @@ -374,7 +335,6 @@ def _get_dictionary_with_corsika_configuration(): "primary_id_type": { "help": "Primary particle ID type", "type": str, - "required": False, "choices": ["common_name", "corsika7_id", "pdg_id"], "default": "common_name", }, @@ -397,12 +357,10 @@ def _get_dictionary_with_corsika_configuration(): "nshow": { "help": "Number of showers per run to simulate.", "type": int, - "required": False, }, "run_number_offset": { "help": "An offset for the run number to be simulated.", "type": int, - "required": False, "default": 0, }, "run_number": { @@ -414,13 +372,11 @@ def _get_dictionary_with_corsika_configuration(): "event_number_first_shower": { "help": "Event number of first shower", "type": int, - "required": False, "default": 1, }, "correct_for_b_field_alignment": { "help": "Correct for B-field alignment", "action": "store_true", - "required": False, "default": True, }, "curved_atmosphere_min_zenith_angle": { @@ -428,96 +384,12 @@ def _get_dictionary_with_corsika_configuration(): "Minimum zenith angle (deg) for using curved-atmosphere CORSIKA binaries. " ), "type": CommandLineParser.zenith_angle, - "required": False, "default": 65 * u.deg, }, } - @staticmethod - def _get_dictionary_with_shower_configuration(): - """Return dictionary with shower configuration parameters.""" - return { - "eslope": { - "help": "Slope of the energy spectrum.", - "type": float, - "required": False, - "default": -2.0, - }, - "energy_range": { - "help": ( - "Energy range of the primary particle (min/max value, e'g', '10 GeV 5 TeV')." - ), - "type": CommandLineParser.parse_quantity_pair, - "required": False, - "default": ["3 GeV 330 TeV"], - }, - "view_cone": { - "help": ( - "View cone radius for primary arrival directions " - "(min/max value, e.g. '0 deg 10 deg')." - ), - "type": CommandLineParser.parse_quantity_pair, - "required": False, - "default": ["0 deg 0 deg"], - }, - "core_scatter": { - "help": "Scatter radius for shower cores (number of use; scatter radius).", - "type": CommandLineParser.parse_integer_and_quantity, - "required": False, - "default": ["10 1400 m"], - }, - } - - @staticmethod - def _get_dictionary_with_sim_telarray_configuration(): - """Return dictionary with sim_telarray configuration parameters.""" - return { - "sim_telarray_instrument_seed": { - "help": "Random seed used for sim_telarray instrument setup.", - "type": CommandLineParser.bounded_int(1, constants.SIMTEL_MAX_SEED), - "required": False, - }, - "sim_telarray_random_instrument_instances": { - "help": "Number of random instrument instances initialized in sim_telarray.", - "type": CommandLineParser.bounded_int(1, 1024), - "required": False, - "default": 1, - }, - "sim_telarray_seed": { - "help": ( - "Random seed used for sim_telarray simulation. " - "Single value: seed for event simulation. " - "Two values: [instrument_seed, simulation_seed] (use for testing only)." - ), - "type": CommandLineParser.bounded_int(1, constants.SIMTEL_MAX_SEED), - "nargs": "+", - "required": False, - }, - # hidden argument to specify the sim_telarray seeds file name - # (defined it here for convenience) - "sim_telarray_seed_file": { - "help": argparse.SUPPRESS, - "type": str, - "required": False, - "default": "sim_telarray_instrument_seeds.txt", - }, - } - - def _initialize_simulation_configuration( - self, group_name, selected_parameters, available_parameters - ): - """ - Initialize simulation configuration arguments. - - Parameters - ---------- - group_name : str - Name of the group of arguments. - selected_parameters : list - List of selected parameters to be added to the group. - available_parameters : dict - Dictionary with available parameters and their configuration. - """ + def _initialize_argument_group(self, group_name, selected_parameters, available_parameters): + """Initialize a group of arguments from a parameter-definition dictionary.""" configuration_group = self.add_argument_group(group_name) if "all" in selected_parameters: @@ -551,14 +423,12 @@ def _add_model_option_layout(job_group, model_options, required=True): help="array layout name(s) (e.g., alpha, subsystem_msts)", nargs="+", type=str, - required=False, ) _layout_group.add_argument( "--array_element_list", help="list of array elements (e.g., LSTN-01, LSTN-02, MSTN).", nargs="+", type=str, - required=False, default=None, ) if "layout_file" in model_options: @@ -567,7 +437,6 @@ def _add_model_option_layout(job_group, model_options, required=True): help="file(s) with the list of array elements (astropy table format).", nargs="+", type=str, - required=False, default=None, ) if "layout_parameter_file" in model_options: @@ -575,7 +444,6 @@ def _add_model_option_layout(job_group, model_options, required=True): "--array_layout_parameter_file", help="Array layout model parameter file (typically in JSON format).", type=str, - required=False, default=None, ) if "plot_all_layouts" in model_options: @@ -583,7 +451,6 @@ def _add_model_option_layout(job_group, model_options, required=True): "--plot_all_layouts", help="plot all available layouts", action="store_true", - required=False, ) return job_group @@ -600,9 +467,7 @@ def _add_model_option_site(self, job_group): ------- argparse.ArgumentParser """ - job_group.add_argument( - "--site", help="site (e.g., North, South)", type=self.site, required=False - ) + job_group.add_argument("--site", help="site (e.g., North, South)", type=self.site) return job_group @staticmethod @@ -706,6 +571,36 @@ def efficiency_interval(value): return fvalue + @staticmethod + def quantity(target_unit): + """ + Build an argument parser type for quantities convertible to a target unit. + + Parameters + ---------- + target_unit : str or astropy.units.UnitBase + Unit to convert the parsed quantity to. + + Returns + ------- + callable + Parser callable returning an ``astropy.units.Quantity``. + """ + target = u.Unit(target_unit) + + def quantity_type(value): + try: + try: + return float(value) * target + except (TypeError, ValueError): + return u.Quantity(value).to(target) + except (TypeError, ValueError, u.UnitConversionError) as exc: + raise argparse.ArgumentTypeError( + f"Invalid quantity value: '{value}'. Expected a value convertible to {target}." + ) from exc + + return quantity_type + @staticmethod def zenith_angle(angle): """ @@ -888,6 +783,108 @@ def bounded_int_type(value): return bounded_int_type +_SHOWER_ARGS = { + "eslope": { + "help": "Slope of the energy spectrum.", + "type": float, + "default": -2.0, + }, + "energy_range": { + "help": "Energy range of the primary particle (min/max value, e'g', '10 GeV 5 TeV').", + "type": CommandLineParser.parse_quantity_pair, + "default": ["3 GeV 330 TeV"], + }, + "view_cone": { + "help": ( + "View cone radius for primary arrival directions (min/max value, e.g. '0 deg 10 deg')." + ), + "type": CommandLineParser.parse_quantity_pair, + "default": ["0 deg 0 deg"], + }, + "core_scatter": { + "help": "Scatter radius for shower cores (number of use; scatter radius).", + "type": CommandLineParser.parse_integer_and_quantity, + "default": ["10 1400 m"], + }, +} + +_SIMTEL_ARGS = { + "sim_telarray_instrument_seed": { + "help": "Random seed used for sim_telarray instrument setup.", + "type": CommandLineParser.bounded_int(1, constants.SIMTEL_MAX_SEED), + }, + "sim_telarray_random_instrument_instances": { + "help": "Number of random instrument instances initialized in sim_telarray.", + "type": CommandLineParser.bounded_int(1, 1024), + "default": 1, + }, + "sim_telarray_seed": { + "help": ( + "Random seed used for sim_telarray simulation. " + "Single value: seed for event simulation. " + "Two values: [instrument_seed, simulation_seed] (use for testing only)." + ), + "type": CommandLineParser.bounded_int(1, constants.SIMTEL_MAX_SEED), + "nargs": "+", + }, + # hidden argument to specify the sim_telarray seeds file name + # (defined it here for convenience) + "sim_telarray_seed_file": { + "help": argparse.SUPPRESS, + "type": str, + "default": "sim_telarray_instrument_seeds.txt", + }, +} + +_APPLICATION_ARGS = { + "source_distance": { + "help": "Source distance in km (unitless values are interpreted as km).", + "type": CommandLineParser.quantity("km"), + "default": 10 * u.km, + }, + "zenith_angle": { + "help": "Zenith angle in degrees (between 0 and 180).", + "type": CommandLineParser.zenith_angle, + "default": 20 * u.deg, + }, + "off_axis_angles": { + "help": ( + "One or more off-axis angles in degrees (unitless values are interpreted as degrees)." + ), + "type": CommandLineParser.quantity("deg"), + "nargs": "+", + "default": [0.0 * u.deg], + }, + "number_of_photons": { + "help": "Number of star photons to trace (per run).", + "type": CommandLineParser.scientific_int, + "default": 10000, + }, + "max_offset": { + "help": "Maximum offset angle in degrees (unitless values are interpreted as deg).", + "type": CommandLineParser.quantity("deg"), + "default": 4 * u.deg, + }, + "offset_step": { + "help": "Offset angle step size in degrees (unitless values are interpreted as deg).", + "type": CommandLineParser.quantity("deg"), + "default": 0.25 * u.deg, + }, + "all_model_versions": { + "help": "Produce reports for all model versions.", + "action": "store_true", + }, + "data": { + "help": "Data file name.", + "type": str, + }, + "telescope_ids": { + "help": "Path to a file containing telescope configurations.", + "type": str, + }, +} + + class BuildInfoAction(argparse.Action): """Custom argparse action to display build information.""" diff --git a/src/simtools/production_configuration/derive_production_statistics_handler.py b/src/simtools/production_configuration/derive_production_statistics_handler.py index 069d42d38f..7dd04c1011 100644 --- a/src/simtools/production_configuration/derive_production_statistics_handler.py +++ b/src/simtools/production_configuration/derive_production_statistics_handler.py @@ -68,22 +68,27 @@ def initialize_evaluators(self): and self.args["zeniths"] and self.args["azimuths"] and self.args["nsb"] - and self.args["offsets"] + and self.args["off_axis_angles"] ): self.logger.warning("No files read") self.logger.warning(f"Base Path: {self.args['base_path']}") self.logger.warning(f"Zeniths: {self.args['zeniths']}") - self.logger.warning(f"Camera offsets: {self.args['offsets']}") + self.logger.warning(f"Camera offsets: {self.args['off_axis_angles']}") return - for zenith, azimuth, nsb, offset in itertools.product( - self.args["zeniths"], self.args["azimuths"], self.args["nsb"], self.args["offsets"] + for zenith, azimuth, nsb, off_axis_angle in itertools.product( + self.args["zeniths"], + self.args["azimuths"], + self.args["nsb"], + self.args["off_axis_angles"], ): + off_axis_angle = u.Quantity(off_axis_angle, u.deg) + offset_value = off_axis_angle.to_value(u.deg) file_name = self.args["file_name_template"].format( zenith=int(zenith), azimuth=azimuth, nsb=nsb, - offset=offset, + offset=offset_value, ) file_path = Path(self.args["base_path"]).joinpath(file_name) @@ -94,7 +99,7 @@ def initialize_evaluators(self): evaluator = StatisticalUncertaintyEvaluator( file_path, metrics=self.metrics, - grid_point=(None, azimuth, zenith, nsb, offset * u.deg), + grid_point=(None, azimuth, zenith, nsb, off_axis_angle), ) evaluator.calculate_metrics() self.evaluator_instances.append(evaluator) diff --git a/src/simtools/ray_tracing/incident_angles.py b/src/simtools/ray_tracing/incident_angles.py index 77a73cc34b..5471a60ba1 100644 --- a/src/simtools/ray_tracing/incident_angles.py +++ b/src/simtools/ray_tracing/incident_angles.py @@ -188,15 +188,23 @@ def _prepare_psf_io_files(self): pf.write( f"# off_axis_angle [deg] = {self.config_data['off_axis_angle'].to_value(u.deg)}\n" ) - pf.write(f"# source_distance [km] = {self.config_data['source_distance']}\n") + distance_km = self._source_distance_km() + pf.write(f"# source_distance [km] = {distance_km}\n") with stars_file.open("w", encoding="utf-8") as sf: zen = self.ZENITH_ANGLE_DEG - dist = float(self.config_data["source_distance"]) - sf.write(f"0. {90.0 - zen} 1.0 {dist}\n") + distance_km = self._source_distance_km() + sf.write(f"0. {90.0 - zen} 1.0 {distance_km}\n") return photons_file, stars_file, log_file + def _source_distance_km(self): + """Return source distance as a scalar value in km.""" + source_distance = self.config_data["source_distance"] + if isinstance(source_distance, u.Quantity): + return source_distance.to_value(u.km) + return float(source_distance) + def _write_run_script(self, photons_file, stars_file, log_file): """Generate a run script for sim_telarray with the provided configuration and inputs. diff --git a/src/simtools/ray_tracing/psf_parameter_optimisation.py b/src/simtools/ray_tracing/psf_parameter_optimisation.py index 2952315ed9..8368ff2bea 100644 --- a/src/simtools/ray_tracing/psf_parameter_optimisation.py +++ b/src/simtools/ray_tracing/psf_parameter_optimisation.py @@ -859,8 +859,8 @@ def _run_ray_tracing_simulation(tel_model, site_model, args_dict, pars): telescope_model=tel_model, site_model=site_model, label=args_dict.get("label") or getattr(tel_model, "label", None), - zenith_angle=args_dict["zenith"] * u.deg, - source_distance=args_dict["src_distance"] * u.km, + zenith_angle=args_dict["zenith_angle"], + source_distance=args_dict["source_distance"], off_axis_angle=[0.0] * u.deg, ) ray.simulate(test=args_dict.get("test", False), force=True) diff --git a/src/simtools/visualization/plot_psf.py b/src/simtools/visualization/plot_psf.py index e6f7070554..d91581a936 100644 --- a/src/simtools/visualization/plot_psf.py +++ b/src/simtools/visualization/plot_psf.py @@ -649,20 +649,30 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp tel_model.overwrite_parameters(best_pars, flat_dict=True) # Create off-axis angle array - max_offset = args_dict.get("max_offset", MAX_OFFSET_DEFAULT) - offset_steps = args_dict.get("offset_steps", OFFSET_STEPS_DEFAULT) + max_offset = args_dict.get("max_offset", MAX_OFFSET_DEFAULT * u.deg) + offset_step = args_dict.get("offset_step", OFFSET_STEPS_DEFAULT * u.deg) + max_offset_deg = ( + max_offset.to_value(u.deg) if isinstance(max_offset, u.Quantity) else max_offset + ) + offset_step_deg = ( + offset_step.to_value(u.deg) if isinstance(offset_step, u.Quantity) else offset_step + ) + if offset_step_deg <= 0: + raise ValueError(f"offset_step must be positive, got {offset_step_deg} deg.") + if max_offset_deg < 0: + raise ValueError(f"max_offset must be non-negative, got {max_offset_deg} deg.") off_axis_angles = np.linspace( 0, - max_offset, - int(max_offset / offset_steps) + 1, + max_offset_deg, + int(max_offset_deg / offset_step_deg) + 1, ) ray = RayTracing( telescope_model=tel_model, site_model=site_model, label=args_dict.get("label") or getattr(tel_model, "label", None), - zenith_angle=args_dict["zenith"] * u.deg, - source_distance=args_dict["src_distance"] * u.km, + zenith_angle=args_dict["zenith_angle"], + source_distance=args_dict["source_distance"], off_axis_angle=off_axis_angles * u.deg, ) @@ -687,7 +697,7 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp plt.ylabel(psf_label_cm if "_cm" in key else psf_label_deg) plt.ylim(bottom=0) plt.xticks(rotation=45) - plt.xlim(0, max_offset) + plt.xlim(0, max_offset_deg) plt.grid(True, alpha=0.3) # Create dynamic file name based on fraction diff --git a/tests/integration_tests/config/derive_psf_parameters_run.yml b/tests/integration_tests/config/derive_psf_parameters_run.yml index 1e796f6365..def9f67c22 100644 --- a/tests/integration_tests/config/derive_psf_parameters_run.yml +++ b/tests/integration_tests/config/derive_psf_parameters_run.yml @@ -8,7 +8,7 @@ applications: site: North telescope: LSTN-01 test: true - zenith: 20 + zenith_angle: 20 integration_tests: - output_file: results/ray_tracing_North_LSTN-01_d10.0km_za20.0deg_derive_psf_parameters.ecsv test_name: run diff --git a/tests/integration_tests/config/production_derive_statistics.yml b/tests/integration_tests/config/production_derive_statistics.yml index 8dd452b1ce..9bdc2b6c0a 100644 --- a/tests/integration_tests/config/production_derive_statistics.yml +++ b/tests/integration_tests/config/production_derive_statistics.yml @@ -10,7 +10,7 @@ applications: metrics_file: tests/resources/production_simulation_config_metrics.yml nsb: - 1 - offsets: + off_axis_angles: - 0 output_file: interpolated_production_statistics.ecsv output_path: simtools-output diff --git a/tests/integration_tests/config/validate_cumulative_psf_prod6_psf.yml b/tests/integration_tests/config/validate_cumulative_psf_prod6_psf.yml index a8c16e712a..22b85de82f 100644 --- a/tests/integration_tests/config/validate_cumulative_psf_prod6_psf.yml +++ b/tests/integration_tests/config/validate_cumulative_psf_prod6_psf.yml @@ -8,7 +8,7 @@ applications: site: North telescope: LSTN-01 test: true - zenith: 20 + zenith_angle: 20 integration_tests: - output_file: results/ray_tracing_North_LSTN-01_d10.0km_za20.0deg_validate_cumulative_psf.ecsv test_name: run diff --git a/tests/integration_tests/config/validate_optics_run.yml b/tests/integration_tests/config/validate_optics_run.yml index 5e3139f631..7393935d4c 100644 --- a/tests/integration_tests/config/validate_optics_run.yml +++ b/tests/integration_tests/config/validate_optics_run.yml @@ -3,14 +3,14 @@ applications: - application: simtools-validate-optics configuration: max_offset: 3.0 - offset_steps: 0.25 + offset_step: 0.25 model_version: 6.0.2 output_path: simtools-output site: North - src_distance: 11.0 + source_distance: 11.0 telescope: LSTN-01 test: true - zenith: 20 + zenith_angle: 20 plot_images: true integration_tests: - output_file: results/ray_tracing_North_LSTN-01_d11.0km_za20.0deg_validate_optics.ecsv diff --git a/tests/unit_tests/configuration/test_commandline_parser.py b/tests/unit_tests/configuration/test_commandline_parser.py index ef59acc7e5..adc0ba4bbb 100644 --- a/tests/unit_tests/configuration/test_commandline_parser.py +++ b/tests/unit_tests/configuration/test_commandline_parser.py @@ -5,6 +5,7 @@ import astropy.units as u import pytest +from astropy.tests.helper import assert_quantity_allclose import simtools.configuration.commandline_parser as parser @@ -214,6 +215,19 @@ def test_azimuth_angle(caplog): assert "The azimuth angle provided is not a valid numerical or string value." in caplog.text +def test_quantity(): + quantity_parser = parser.CommandLineParser.quantity("km") + + assert quantity_parser("10") == 10 * u.km + assert_quantity_allclose(quantity_parser("1500 m"), 1.5 * u.km) + + with pytest.raises( + argparse.ArgumentTypeError, + match=r"Invalid quantity value: 'invalid'. Expected a value convertible to km.", + ): + quantity_parser("invalid") + + def test_initialize_default_arguments(): # default arguments _parser_1 = parser.CommandLineParser() @@ -236,6 +250,53 @@ def test_initialize_default_arguments(): assert "output" in [str(group.title) for group in job_groups] +def test_initialize_application_arguments(): + app_parser = parser.CommandLineParser() + app_parser.initialize_application_arguments( + [ + "source_distance", + "zenith_angle", + "number_of_photons", + "off_axis_angles", + "all_model_versions", + "data", + "telescope_ids", + ] + ) + + args = app_parser.parse_args( + [ + "--source_distance", + "1500 m", + "--zenith_angle", + "25 deg", + "--number_of_photons", + "1e6", + "--off_axis_angles", + "0.5", + "1 deg", + "--all_model_versions", + "--data", + "psf_data.ecsv", + "--telescope_ids", + "layout_ids.txt", + ] + ) + + assert_quantity_allclose(args.source_distance, 1.5 * u.km) + assert_quantity_allclose(args.zenith_angle, 25 * u.deg) + assert args.number_of_photons == 1_000_000 + assert len(args.off_axis_angles) == 2 + assert_quantity_allclose(args.off_axis_angles[0], 0.5 * u.deg) + assert_quantity_allclose(args.off_axis_angles[1], 1 * u.deg) + assert args.all_model_versions is True + assert args.data == "psf_data.ecsv" + assert args.telescope_ids == "layout_ids.txt" + + job_groups = app_parser._action_groups + assert "application" in [str(group.title) for group in job_groups] + + def test_simulation_model(): # simulation model is none _parser_n = parser.CommandLineParser() @@ -383,7 +444,6 @@ def test_get_dictionary_with_corsika_configuration(mocker): assert "primary_id_type" in corsika_config assert corsika_config["primary_id_type"]["help"] == "Primary particle ID type" assert corsika_config["primary_id_type"]["type"] is str - assert corsika_config["primary_id_type"]["required"] is False assert corsika_config["primary_id_type"]["choices"] == ["common_name", "corsika7_id", "pdg_id"] assert corsika_config["primary_id_type"]["default"] == "common_name" @@ -405,7 +465,6 @@ def test_get_dictionary_with_corsika_configuration(mocker): assert "nshow" in corsika_config assert corsika_config["nshow"]["help"] == "Number of showers per run to simulate." assert corsika_config["nshow"]["type"] is int - assert corsika_config["nshow"]["required"] is False # Test the "run_number_offset" key assert "run_number_offset" in corsika_config @@ -414,7 +473,6 @@ def test_get_dictionary_with_corsika_configuration(mocker): == "An offset for the run number to be simulated." ) assert corsika_config["run_number_offset"]["type"] is int - assert corsika_config["run_number_offset"]["required"] is False assert corsika_config["run_number_offset"]["default"] == 0 # Test the "run_number" key @@ -428,7 +486,6 @@ def test_get_dictionary_with_corsika_configuration(mocker): assert "event_number_first_shower" in corsika_config assert corsika_config["event_number_first_shower"]["help"] == "Event number of first shower" assert corsika_config["event_number_first_shower"]["type"] is int - assert corsika_config["event_number_first_shower"]["required"] is False assert corsika_config["event_number_first_shower"]["default"] == 1 # Test the "correct_for_b_field_alignment" key @@ -437,7 +494,6 @@ def test_get_dictionary_with_corsika_configuration(mocker): corsika_config["correct_for_b_field_alignment"]["help"] == "Correct for B-field alignment" ) assert corsika_config["correct_for_b_field_alignment"]["action"] == "store_true" - assert corsika_config["correct_for_b_field_alignment"]["required"] is False assert corsika_config["correct_for_b_field_alignment"]["default"] is True diff --git a/tests/unit_tests/production_configuration/test_derive_production_statistics_handler.py b/tests/unit_tests/production_configuration/test_derive_production_statistics_handler.py index 313acb6463..8641748230 100644 --- a/tests/unit_tests/production_configuration/test_derive_production_statistics_handler.py +++ b/tests/unit_tests/production_configuration/test_derive_production_statistics_handler.py @@ -51,7 +51,7 @@ def args_dict(tmp_path, metrics_file, grid_points_file): "zeniths": [20, 40], "azimuths": [180], "nsb": [0.005], - "offsets": [0.5, 1.0], + "off_axis_angles": [0.5, 1.0], "query_point": [1.0, 180.0, 20.0, 4.0, 0.5], "output_file": "production_statistics.ecsv", "output_path": str(tmp_path), @@ -127,7 +127,7 @@ def test_no_base_path(mock_handler): def test_empty_offsets(mock_handler): """Test behavior when offsets are empty.""" - mock_handler.args["offsets"] = [] # Empty offsets + mock_handler.args["off_axis_angles"] = [] # Empty offsets mock_handler.initialize_evaluators() @@ -233,7 +233,7 @@ def test_handler_with_grid_points_from_file(grid_points_file, metrics_file, tmp_ args_dict = { "base_path": BASE_PATH, "zeniths": [20, 40], - "offsets": [0.5], + "off_axis_angles": [0.5], "azimuths": [0], "nsb": [0.005], "grid_points_production_file": str(grid_points_file), @@ -266,7 +266,7 @@ def test_empty_grid_points_production_file(metrics_file, tmp_path): args_dict = { "base_path": BASE_PATH, "zeniths": [20, 40], - "offsets": [0.5], + "off_axis_angles": [0.5], "azimuths": [0], "nsb": [0.005], "grid_points_production_file": str(grid_points_file), @@ -289,7 +289,7 @@ def test_grid_points_with_incorrect_format(metrics_file, tmp_path): args_dict = { "base_path": BASE_PATH, "zeniths": [20, 40], - "offsets": [0.5], + "off_axis_angles": [0.5], "azimuths": [0], "nsb": [0.005], "grid_points_production_file": str(grid_points_file), @@ -308,7 +308,7 @@ def test_initialize_evaluators_with_valid_files(mock_handler): mock_handler.args["zeniths"] = [20, 40] mock_handler.args["azimuths"] = [0] mock_handler.args["nsb"] = [0.005] - mock_handler.args["offsets"] = [0.5, 1.0] + mock_handler.args["off_axis_angles"] = [0.5, 1.0] mock_handler.args["base_path"] = "test/path" mock_handler.args["file_name_template"] = "test_{zenith}.fits" mock_handler.evaluator_instances = [] @@ -327,14 +327,16 @@ def test_initialize_evaluators_with_valid_files(mock_handler): ), ): mock_handler.initialize_evaluators() - expected_count = ( - len(mock_handler.args["zeniths"]) - * len(mock_handler.args["azimuths"]) - * len(mock_handler.args["nsb"]) - * len(mock_handler.args["offsets"]) - ) - assert len(mock_handler.evaluator_instances) == expected_count - assert mock_evaluator_instance.calculate_metrics.call_count == expected_count + + expected_count = ( + len(mock_handler.args["zeniths"]) + * len(mock_handler.args["azimuths"]) + * len(mock_handler.args["nsb"]) + * len(mock_handler.args["off_axis_angles"]) + ) + assert len(mock_handler.evaluator_instances) == expected_count + + assert mock_evaluator_instance.calculate_metrics.call_count == expected_count def test_perform_interpolation_with_grid_points(mock_handler): diff --git a/tests/unit_tests/ray_tracing/test_incident_angles.py b/tests/unit_tests/ray_tracing/test_incident_angles.py index 2bdc44bc27..4a6ee815e2 100644 --- a/tests/unit_tests/ray_tracing/test_incident_angles.py +++ b/tests/unit_tests/ray_tracing/test_incident_angles.py @@ -23,7 +23,7 @@ def config_data(): "site": "North", "model_version": "prod6", "off_axis_angle": 0.0 * u.deg, - "source_distance": 10.0, # km + "source_distance": 10.0 * u.km, "number_of_photons": 1000, } diff --git a/tests/unit_tests/ray_tracing/test_psf_parameter_optimisation.py b/tests/unit_tests/ray_tracing/test_psf_parameter_optimisation.py index e490bd0814..ea8e9f0788 100644 --- a/tests/unit_tests/ray_tracing/test_psf_parameter_optimisation.py +++ b/tests/unit_tests/ray_tracing/test_psf_parameter_optimisation.py @@ -6,6 +6,7 @@ import numpy as np import pytest +from astropy import units as u import simtools.ray_tracing.psf_parameter_optimisation as psf_opt @@ -55,8 +56,8 @@ def mock_args_dict(): "learning_rate": 0.1, "test": True, "plot_all": False, - "zenith": 20.0, - "src_distance": 10.0, + "zenith_angle": 20.0 * u.deg, + "source_distance": 10.0 * u.km, "monte_carlo_analysis": False, "rmsd_threshold": 0.01, "fraction": 0.8, diff --git a/tests/unit_tests/test_application_control.py b/tests/unit_tests/test_application_control.py index b0dbbdf3f7..a3f9b03619 100644 --- a/tests/unit_tests/test_application_control.py +++ b/tests/unit_tests/test_application_control.py @@ -11,8 +11,10 @@ from simtools.application_control import ( _resolve_model_version_to_latest_patch, _version_info, + build_application, get_application_label, get_log_file, + get_module_description_line, setup_logging, startup_application, ) @@ -169,6 +171,122 @@ def test_get_application_label(file_path, expected): assert result == expected +@pytest.mark.parametrize( + ("docstring", "expected"), + [ + ("Short description.\n\nMore details.\n", "Short description."), + (" Short description.\n\nMore details.\n", "Short description."), + ("\nShort description on next line.\n\nMore details.\n", "Short description on next line."), + ( + " \n Short description with indent.\n\n More details.\n", + "Short description with indent.", + ), + ], +) +def test_get_module_description_line(docstring, expected): + """Test module description extraction from docstring.""" + assert get_module_description_line(docstring) == expected + + +def test_get_module_description_line_without_docstring(): + """Test module description extraction error on missing or empty docstring.""" + with pytest.raises(ValueError, match="Missing or empty docstring"): + get_module_description_line(None) + + with pytest.raises(ValueError, match="Empty docstring"): + get_module_description_line(" \n \n") + + +def test_build_application(mocker, tmp_test_directory): + """Test build_application wraps Configurator and startup_application.""" + startup_mock = mocker.patch( + "simtools.application_control.startup_application", + return_value="app_context", + ) + configurator_class = mocker.patch("simtools.application_control.configurator.Configurator") + configurator_instance = configurator_class.return_value + configurator_instance.initialize.return_value = ({"log_level": "info"}, {}) + add_arguments = MagicMock() + + result = build_application( + str(tmp_test_directory / "test_application.py"), + description="Test description", + add_arguments_function=add_arguments, + initialization_kwargs={"output": True}, + startup_kwargs={"setup_io_handler": False}, + ) + + assert result == "app_context" + startup_mock.assert_called_once() + + parse_function = startup_mock.call_args.args[0] + startup_kwargs = startup_mock.call_args.kwargs + + assert startup_kwargs == {"setup_io_handler": False} + assert parse_function() == ({"log_level": "info"}, {}) + configurator_class.assert_called_once_with( + label="test_application", + usage=None, + description="Test description", + epilog=None, + ) + add_arguments.assert_called_once_with(configurator_instance.parser) + configurator_instance.initialize.assert_called_once_with(output=True) + + +def test_build_application_infers_caller_metadata(mocker): + """Test build_application infers file/doc/add_arguments from caller module.""" + startup_mock = mocker.patch( + "simtools.application_control.startup_application", + return_value="app_context", + ) + configurator_class = mocker.patch("simtools.application_control.configurator.Configurator") + configurator_instance = configurator_class.return_value + configurator_instance.initialize.return_value = ({"log_level": "info"}, {}) + add_arguments = MagicMock() + + globals()["_add_arguments"] = add_arguments + try: + result = build_application( + initialization_kwargs={"output": True}, + startup_kwargs={"setup_io_handler": False}, + ) + finally: + del globals()["_add_arguments"] + + assert result == "app_context" + startup_mock.assert_called_once() + parse_function = startup_mock.call_args.args[0] + startup_kwargs = startup_mock.call_args.kwargs + + assert startup_kwargs == {"setup_io_handler": False} + assert parse_function() == ({"log_level": "info"}, {}) + configurator_class.assert_called_once_with( + label="test_application_control", + usage=None, + description="Unit tests for application_control module.", + epilog=None, + ) + add_arguments.assert_called_once_with(configurator_instance.parser) + configurator_instance.initialize.assert_called_once_with(output=True) + + +def test_build_application_missing_metadata_raises(mocker, tmp_test_directory): + """Test build_application raises if inference and explicit metadata are unavailable.""" + startup_mock = mocker.patch("simtools.application_control.startup_application") + + with patch("simtools.application_control.inspect.currentframe") as frame_mock: + frame_mock.return_value.f_back.f_globals = {} + + with pytest.raises(ValueError, match="Missing application path"): + build_application(description="test") + + with pytest.raises(ValueError, match="Missing description"): + build_application(application_path=tmp_test_directory / "test.py") + + startup_mock.assert_not_called() + + def test_startup_application_basic(): """Test startup_application function with basic configuration.""" # Mock parse function diff --git a/tests/unit_tests/visualization/test_plot_psf.py b/tests/unit_tests/visualization/test_plot_psf.py index baa5c8e39b..156c53d33e 100644 --- a/tests/unit_tests/visualization/test_plot_psf.py +++ b/tests/unit_tests/visualization/test_plot_psf.py @@ -6,6 +6,7 @@ import matplotlib.pyplot as plt import numpy as np import pytest +from astropy import units as u from simtools.visualization import plot_psf @@ -287,8 +288,8 @@ def test_create_psf_vs_offaxis_plot(sample_parameters, tmp_path): mock_site_model = MagicMock() args_dict = { "fraction": 0.8, - "zenith": 20, - "src_distance": 10, + "zenith_angle": 20 * u.deg, + "source_distance": 10 * u.km, } # Mock RayTracing and its methods @@ -442,11 +443,54 @@ def test_create_summary_psf_comparison_plot(tmp_path, sample_psf_data, sample_pa # Verify title contains parameter information title_call = mock_ax.set_title.call_args[0][0] assert "Final Optimized Parameters" in title_call - assert "mirror_reflection_random_angle" in title_call - assert "mirror_align_random_vertical" in title_call - assert "mirror_align_random_horizontal" in title_call - - # Verify RMSD text - text_call = mock_ax.text.call_args[0][2] - assert "RMSD" in text_call - assert "0.0230" in text_call + + +def test_create_psf_vs_offaxis_plot_zero_offset_step(sample_parameters): + """Zero offset_step must raise ValueError before reaching np.linspace.""" + mock_telescope_model = MagicMock() + mock_site_model = MagicMock() + args_dict = { + "fraction": 0.8, + "zenith_angle": 20 * u.deg, + "source_distance": 10 * u.km, + "max_offset": 4.0 * u.deg, + "offset_step": 0.0 * u.deg, + } + with pytest.raises(ValueError, match="offset_step must be positive"): + plot_psf.create_psf_vs_offaxis_plot( + mock_telescope_model, mock_site_model, args_dict, sample_parameters, None + ) + + +def test_create_psf_vs_offaxis_plot_negative_offset_step(sample_parameters): + """Negative offset_step must raise ValueError before reaching np.linspace.""" + mock_telescope_model = MagicMock() + mock_site_model = MagicMock() + args_dict = { + "fraction": 0.8, + "zenith_angle": 20 * u.deg, + "source_distance": 10 * u.km, + "max_offset": 4.0 * u.deg, + "offset_step": -0.5 * u.deg, + } + with pytest.raises(ValueError, match="offset_step must be positive"): + plot_psf.create_psf_vs_offaxis_plot( + mock_telescope_model, mock_site_model, args_dict, sample_parameters, None + ) + + +def test_create_psf_vs_offaxis_plot_negative_max_offset(sample_parameters): + """Negative max_offset must raise ValueError before reaching np.linspace.""" + mock_telescope_model = MagicMock() + mock_site_model = MagicMock() + args_dict = { + "fraction": 0.8, + "zenith_angle": 20 * u.deg, + "source_distance": 10 * u.km, + "max_offset": -1.0 * u.deg, + "offset_step": 0.5 * u.deg, + } + with pytest.raises(ValueError, match="max_offset must be non-negative"): + plot_psf.create_psf_vs_offaxis_plot( + mock_telescope_model, mock_site_model, args_dict, sample_parameters, None + )