Source code for run_application

#!/usr/bin/python3

"""
Run several simtools applications using a configuration file.

Allows to run several simtools applications with a single configuration file, which includes
both the name of the simtools application and the configuration for the application.

This application is used for model parameter setting workflows.
Strong assumptions are applied on the directory structure for input and output files of
applications.

Example
-------

Run the application with the configuration file 'config_file_name':

.. code-block:: console

    simtools-run-application --configuration_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

"""

import logging
import subprocess
import tempfile
from pathlib import Path

import yaml

import simtools.utils.general as gen
from simtools import dependencies
from simtools.configuration import configurator


def _parse(label, description, usage):
    """
    Parse command line configuration.

    Parameters
    ----------
    label : str
        Label describing the application.
    description : str
        Description of the application.
    usage : str
        Example on how to use the application.

    Returns
    -------
    CommandLineParser
        Command line parser object.
    """
    config = configurator.Configurator(label=label, description=description, usage=usage)

    config.parser.add_argument(
        "--configuration_file",
        help="Application configuration.",
        type=str,
        required=True,
        default=None,
    )
    config.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).",
    )
    return config.initialize(db_config=True)


[docs] def run_application(application, configuration, logger): """Run a simtools application and return stdout and stderr.""" with tempfile.NamedTemporaryFile(mode="w", delete=True, suffix=".yml") as temp_config: yaml.dump(configuration, temp_config, default_flow_style=False) temp_config.flush() configuration_file = Path(temp_config.name) try: result = subprocess.run( [application, "--config", configuration_file], check=True, capture_output=True, text=True, ) except subprocess.CalledProcessError as exc: logger.error(f"Error running application {application}: {exc.stderr}") raise exc return result.stdout, result.stderr
[docs] def get_subdirectory_name(path): """Get the first subdirectory name under 'input'.""" path = Path(path).resolve() try: input_index = path.parts.index("input") return path.parts[input_index], path.parts[input_index + 1] except (ValueError, IndexError) as exc: raise ValueError(f"Could not find subdirectory under 'input': {exc}") from exc
[docs] def read_application_configuration(configuration_file, steps, logger): """ Read application configuration from file and modify for setting workflows. Strong assumptions on the structure of input and output files: - configuration file is expected to be in './input/<workflow directory>/<yaml file>' - output files will be written out to './output/<workflow directory>/' Replaces the placeholders in the configuration file with the actual values. Sets 'USE_PLAIN_OUTPUT_PATH' to True for all applications. Parameters ---------- configuration_file : str Configuration file name. steps : list List of steps to be executed (None: all steps). logger : Logger Logger object. Returns ------- dict Application configuration. Path Path to the log file. """ application_config = gen.collect_data_from_file(configuration_file).get("CTA_SIMPIPE") place_holder = "__SETTING_WORKFLOW__" workflow_dir, setting_workflow = get_subdirectory_name(configuration_file) output_path = Path(str(workflow_dir).replace("input", "output")) / Path(setting_workflow) output_path.mkdir(parents=True, exist_ok=True) logger.info(f"Setting workflow output path to {output_path}") log_file = output_path / "simtools.log" configurations = application_config.get("APPLICATIONS") for step_count, config in enumerate(configurations, start=1): config["RUN_APPLICATION"] = step_count in steps if steps else True for key, value in config.get("CONFIGURATION", {}).items(): if isinstance(value, str): config["CONFIGURATION"][key] = value.replace(place_holder, setting_workflow) if isinstance(value, list): config["CONFIGURATION"][key] = [ item.replace(place_holder, setting_workflow) if isinstance(item, str) else item for item in value ] config["CONFIGURATION"]["USE_PLAIN_OUTPUT_PATH"] = True config["CONFIGURATION"]["OUTPUT_PATH"] = str(output_path) return configurations, log_file
def main(): # noqa: D103 args_dict, db_config = _parse( Path(__file__).stem, description="Run simtools applications using a configuration file.", usage="simtools-run-application --config_file config_file_name", ) logger = logging.getLogger() logger.setLevel(gen.get_log_level_from_user(args_dict["log_level"])) configurations, log_file = read_application_configuration( args_dict["configuration_file"], args_dict["steps"], logger ) with log_file.open("w", encoding="utf-8") as file: file.write("Running simtools applications\n") file.write(dependencies.get_version_string(db_config)) for config in configurations: if config.get("RUN_APPLICATION"): logger.info(f"Running application: {config.get('APPLICATION')}") else: logger.info(f"Skipping application: {config.get('APPLICATION')}") continue config = gen.change_dict_keys_case(config, False) stdout, stderr = run_application( config.get("APPLICATION"), config.get("CONFIGURATION"), logger ) file.write("=" * 80 + "\n") file.write(f"Application: {config.get('APPLICATION')}\n") file.write("STDOUT:\n" + stdout) file.write("STDERR:\n" + stderr) if __name__ == "__main__": main()