Repository: gammasim/simtools (language: python)

Issue:
Simulator configurator unecessary complicated and requires cross-module knowledge of data structures
The configuration of the Simulator tools through a yml file (e.g., ./tests/resources/prod_multi_config_test_North.yml) is unnecessary complicated. 

The config dictionaries are modified in the applications (e.g., production.py or simulate_prod.py) and in Simulator in order to generate the right format for CorsikaRunner and SimtelRunner. High-level tools / modules need therefore to know exactly the data structure of the low level tools. 

At several levels, there is a large number of modifications to the configuration dicts, including fine-tuned additions or pops. Very hard to maintain and we need to simplify this. 


Discussion / hints:
Also requires a metaschema to validate the input configuration.

Fix patch:
diff --git a/data/parameters/array-simulator_parameters.yml b/data/parameters/array-simulator_parameters.yml
deleted file mode 100644
index e28d968a4..000000000
--- a/data/parameters/array-simulator_parameters.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-data_directory:
-  len: 1
-  unit: null
-  default: null
-primary:
-  len: 1
-  unit: null
-zenith_angle:
-  len: 1
-  unit: 'deg'
-  names: ['zenith', 'theta']
-azimuth_angle:
-  len: 1
-  unit: 'deg'
-  names: ['azimuth', 'phi']
diff --git a/data/parameters/corsika_parameters.yml b/data/parameters/corsika_parameters.yml
index 43812e44f..16ddbfb52 100644
--- a/data/parameters/corsika_parameters.yml
+++ b/data/parameters/corsika_parameters.yml
@@ -1,42 +1,3 @@
-USER_PARAMETERS:
-  RUNNR:
-    len: 1
-    names: ['RUNNUMBER', 'NRUN']
-    default: 1
-  EVTNR:
-    len: 1
-    names: ['EVENTNR', 'EVENTNUMBER']
-    default: 1
-  NSHOW:
-    len: 1
-    names: ['NSHOWERS']
-  PRMPAR:
-    len: 1
-    names: ['PRIMARY']
-  ERANGE:
-    len: 2
-    names: ['ENRANGE', 'ENERGYRANGE']
-    unit: ['GeV', 'GeV']
-  ESLOPE:
-    len: 1
-    names: ['ENSLOPE', 'ENERGYSLOPE']
-  THETAP:
-    len: 2
-    names: ['THETA', 'ZENITH']
-    unit: ['deg', 'deg']
-  AZM:
-    len: 2
-    names: ['PHI', 'AZIMUTH']
-    unit: ['deg', 'deg']
-  VIEWCONE:
-    len: 2
-    names: ['CONE']
-    unit: ['deg', 'deg']
-  CSCAT:
-    len: 3
-    names: ['SCAT']
-    unit: [null, 'cm', null]
-
 PRIMARIES:
   GAMMA:
     number: 1
@@ -70,7 +31,6 @@ INTERACTION_FLAGS:
   LONGI: ['T', 20., 'F', 'F']
   MAXPRT: [0]
   ECTMAP: [1.e6]
-#  STEPFC: [1.0]
 
 CHERENKOV_EMISSION_PARAMETERS:
   CERSIZ: [5.]
@@ -83,8 +43,6 @@ DEBUGGING_OUTPUT_PARAMETERS:
   DIRECT: ['/dev/null']
 
 IACT_TUNING_PARAMETERS:
-  IACT: [
-    ['SPLIT_AUTO', '15M'],
-    ['IO_BUFFER', '800MB'],
-    ['MAX_BUNCHES', '1000000']
-  ]
+  SPLIT_AUTO: ['15M']
+  IO_BUFFER: ['800MB']
+  MAX_BUNCHES: ['1000000']
diff --git a/data/parameters/simtel-runner-array_parameters.yml b/data/parameters/simtel-runner-array_parameters.yml
deleted file mode 100644
index 842cd9a97..000000000
--- a/data/parameters/simtel-runner-array_parameters.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-simtel_data_directory:
-  len: 1
-  unit: null
-  default: null
-primary:
-  len: 1
-  unit: null
-zenith_angle:
-  len: 1
-  unit: 'deg'
-  default:
-    value: 20
-    unit: 'deg'
-  names: ['zenith', 'theta']
-azimuth_angle:
-  len: 1
-  unit: 'deg'
-  default:
-    value: 0
-    unit: 'deg'
-  names: ['azimuth', 'phi']
diff --git a/docs/source/api-reference/corsika.md b/docs/source/api-reference/corsika.md
index 1a7cfc777..9e9598d5b 100644
--- a/docs/source/api-reference/corsika.md
+++ b/docs/source/api-reference/corsika.md
@@ -13,16 +13,6 @@ Support modules for running CORSIKA.
    :members:
 ```
 
-## corsika_default_config
-
-(corsika-default-config-1)=
-
-```{eval-rst}
-.. automodule:: corsika.corsika_default_config
-   :members:
-
-```
-
 ## corsika_histograms
 
 (corsika-histograms-1)=
@@ -41,12 +31,3 @@ Support modules for running CORSIKA.
 .. automodule:: corsika.corsika_histograms_visualize
    :members:
 ```
-
-## corsika_runner
-
-(corsika-runner-1)=
-
-```{eval-rst}
-.. automodule:: corsika.corsika_runner
-   :members:
-```
diff --git a/docs/source/api-reference/corsika_simtel.md b/docs/source/api-reference/corsika_simtel.md
deleted file mode 100644
index 854670cbf..000000000
--- a/docs/source/api-reference/corsika_simtel.md
+++ /dev/null
@@ -1,14 +0,0 @@
-(corsikasimtel)=
-
-# CORSIKA and sim_telarray
-
-Support module for running CORSIKA and sim_telarray.
-
-## corsika_simtel_runner
-
-(corsika-simtel-runner-1)=
-
-```{eval-rst}
-.. automodule:: corsika_simtel.corsika_simtel_runner
-   :members:
-```
diff --git a/docs/source/api-reference/index.md b/docs/source/api-reference/index.md
index 1a698034f..13d87d8e1 100644
--- a/docs/source/api-reference/index.md
+++ b/docs/source/api-reference/index.md
@@ -11,7 +11,6 @@ simtools is composed of the following modules and module groups:
 camera_efficiency
 configuration_module
 corsika
-corsika_simtel
 data_model
 db_handler
 io_operations
@@ -20,6 +19,7 @@ array_layout
 mc_model
 psf_analysis
 ray_tracing
+runners
 sim_telarray
 simulators
 utils
diff --git a/docs/source/api-reference/runners.md b/docs/source/api-reference/runners.md
new file mode 100644
index 000000000..4f6b40478
--- /dev/null
+++ b/docs/source/api-reference/runners.md
@@ -0,0 +1,41 @@
+(runners)=
+
+# Runners for simulation software.
+
+Support module for running CORSIKA and sim_telarray.
+
+## corsika_runner
+
+(corsika-runner-1)=
+
+```{eval-rst}
+.. automodule:: runners.corsika_runner
+   :members:
+```
+
+## simtel_runner
+
+(simtel-runner-1)=
+
+```{eval-rst}
+.. automodule:: runners.simtel_runner
+   :members:
+```
+
+## corsika_simtel_runner
+
+(corsika-simtel-runner-1)=
+
+```{eval-rst}
+.. automodule:: runners.corsika_simtel_runner
+   :members:
+```
+
+## runner_services
+
+(runner-services1)=
+
+```{eval-rst}
+.. automodule:: runners.runner_services
+   :members:
+```
diff --git a/docs/source/api-reference/sim_telarray.md b/docs/source/api-reference/sim_telarray.md
index b6ff40765..4c997f523 100644
--- a/docs/source/api-reference/sim_telarray.md
+++ b/docs/source/api-reference/sim_telarray.md
@@ -49,15 +49,6 @@ Support modules for running sim_telarray.
    :members:
 ```
 
-## simtel_runner
-
-(simtel-runner-1)=
-
-```{eval-rst}
-.. automodule:: simtel.simtel_runner
-   :members:
-```
-
 ## simulator_array
 
 (simulate-array-1)=
diff --git a/docs/source/user-guide/applications.md b/docs/source/user-guide/applications.md
index 79da92ebc..03ac3b5d4 100644
--- a/docs/source/user-guide/applications.md
+++ b/docs/source/user-guide/applications.md
@@ -56,7 +56,6 @@ generate_regular_arrays <applications/generate_regular_arrays>
 plot_array_layout <applications/plot_array_layout>
 simulate_light_emission <applications/simulate_light_emission>
 simulate_prod <applications/simulate_prod>
-simulate_showers_for_trigger_rates <applications/simulate_showers_for_trigger_rates>
 submit_data_from_external <applications/submit_data_from_external>
 validate_camera_efficiency <applications/validate_camera_efficiency>
 validate_camera_fov <applications/validate_camera_fov>
diff --git a/docs/source/user-guide/applications/simulate_showers_for_trigger_rates.rst b/docs/source/user-guide/applications/simulate_showers_for_trigger_rates.rst
deleted file mode 100644
index d5d64d489..000000000
--- a/docs/source/user-guide/applications/simulate_showers_for_trigger_rates.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-
-simulate_showers_for_trigger_rates
-==================================
-
-.. automodule:: simulate_showers_for_trigger_rates
-   :members:
diff --git a/simtools/applications/simulate_prod.py b/simtools/applications/simulate_prod.py
index 6ab5bde3b..78eede8cf 100644
--- a/simtools/applications/simulate_prod.py
+++ b/simtools/applications/simulate_prod.py
@@ -1,27 +1,19 @@
 #!/usr/bin/python3
 
 r"""
-    Run simulations for productions (typically on the grid).
+    Generate simulation configuration and run simulations (if required).
 
-    It allows to run a Paranal (CTAO-South) or La Palma (CTAO-North) array layout simulation
-    with the provided "prod_tag" simulation configuration (e.g., Prod6)
-    for a given primary particle, azimuth, and zenith angle.
+    Multipipe scripts will be produced as part of this application.
+    Allows to run array layout simulation including shower and detector simulations
 
-    The entire simulation chain is performed, i.e., shower simulations with CORSIKA
-    which are piped directly to sim_telarray using the sim_telarray multipipe mechanism.
-    This script produces all the necessary configuration files for CORSIKA and
-    sim_telarray before running simulation.
-    The multipipe scripts will be produced as part of this script.
+    The entire simulation chain, parts of it, or nothing is executed:
 
-    This script does not provide a mechanism to submit jobs to a batch system like others
-    in simtools since it is meant to be executed on a grid node
-    (distributed to it by the workload management system).
+    - shower simulations with CORSIKA only
+    - shower simulations with CORSIKA which are piped directly to sim_telarray using
+      the sim_telarray multipipe mechanism.
 
     Command line arguments
     ----------------------
-    production_config (str, Path, required)
-        Simulation configuration file
-        (contains the default setup which can be overwritten by the command line options).
     model_version (str, required)
         The telescope model version to use (e.g., Prod5).
     site (str, required)
@@ -63,9 +55,8 @@
     .. code-block:: console
 
         simtools-simulate-prod \\
-        --production_config tests/resources/prod_multi_config_test.yml --model_version Prod5 \\
-        --site north --primary gamma --azimuth_angle north --zenith_angle 20 \\
-        --start_run 0 --run 1
+        --model_version Prod5 --site north --primary gamma --azimuth_angle north \\
+        --zenith_angle 20 --start_run 0 --run 1
 
     By default the configuration is saved in simtools-output/test-production
     together with the actual simulation output in corsika-data and simtel-data within.
@@ -73,24 +64,6 @@
     to a different location via the option --data_directory,
     but the label is always added to the data_directory, such that the output
     will be written to data_directory/label/simtel-data.
-
-    Expected final print-out message:
-
-    .. code-block:: console
-
-        INFO::array_layout(l569)::read_telescope_list_file::Reading array elements from ...
-        WARNING::corsika_runner(l127)::_load_corsika_config_data::data_directory not given
-        in corsika_config - default output directory will be set.
-        INFO::array_layout(l569)::read_telescope_list_file::Reading array elements from ...
-        INFO::corsika_config(l493)::_set_output_file_and_directory::Creating directory
-        INFO::simulator(l405)::simulate::Submission command: local
-        INFO::simulator(l410)::simulate::Starting submission for 1 run
-        INFO::array_model(l315)::export_simtel_array_config_file::Writing array config file into
-        INFO::job_manager(l95)::submit::Submitting script
-        INFO::job_manager(l96)::submit::Job output stream
-        INFO::job_manager(l97)::submit::Job error stream
-        INFO::job_manager(l98)::submit::Job log stream
-        INFO::job_manager(l119)::_submit_local::Running script locally
 """
 
 import logging
@@ -100,7 +73,6 @@
 
 import simtools.utils.general as gen
 from simtools.configuration import configurator
-from simtools.configuration.commandline_parser import CommandLineParser
 from simtools.simulator import Simulator
 
 
@@ -120,80 +92,6 @@ def _parse(description=None):
 
     """
     config = configurator.Configurator(description=description)
-    config.parser.add_argument(
-        "--production_config",
-        help=(
-            "Simulation configuration file "
-            "(contains the default setup which can be overwritten by the command line options)"
-        ),
-        type=str,
-        required=True,
-    )
-    config.parser.add_argument(
-        "--simulation_software",
-        help="Simulation software steps.",
-        type=str,
-        choices=["corsika", "simtel", "corsika_simtel"],
-        required=True,
-        default="corsika_simtel",
-    )
-    config.parser.add_argument(
-        "--primary",
-        help="Primary particle to simulate.",
-        type=str.lower,
-        required=True,
-        choices=[
-            "gamma",
-            "gamma_diffuse",
-            "electron",
-            "proton",
-            "muon",
-            "helium",
-            "nitrogen",
-            "silicon",
-            "iron",
-        ],
-    )
-    config.parser.add_argument(
-        "--azimuth_angle",
-        help=(
-            "Telescope pointing direction in azimuth. "
-            "It can be in degrees between 0 and 360 or one of north, south, east or west "
-            "(case insensitive). Note that North is 0 degrees and "
-            "the azimuth grows clockwise, so East is 90 degrees."
-        ),
-        type=CommandLineParser.azimuth_angle,
-        required=True,
-    )
-    config.parser.add_argument(
-        "--zenith_angle",
-        help="Zenith angle in degrees (between 0 and 180).",
-        type=CommandLineParser.zenith_angle,
-        required=True,
-    )
-    config.parser.add_argument(
-        "--nshow",
-        help="Number of showers to simulate.",
-        type=int,
-        required=False,
-    )
-    config.parser.add_argument(
-        "--start_run",
-        help=(
-            "Start run number such that the actual run number will be 'start_run' + 'run'. "
-            "This is useful in case a new transform is submitted for the same production. "
-            "It allows the transformation system to keep using sequential run numbers without "
-            "repetition."
-        ),
-        type=int,
-        required=True,
-    )
-    config.parser.add_argument(
-        "--run",
-        help="Run number (actual run number will be 'start_run' + 'run')",
-        type=int,
-        required=True,
-    )
     config.parser.add_argument(
         "--data_directory",
         help=(
@@ -212,7 +110,47 @@ def _parse(description=None):
         required=False,
         default=False,
     )
-    return config.initialize(db_config=True, simulation_model="telescope")
+    return config.initialize(
+        db_config=True,
+        simulation_model=["site", "layout", "telescope"],
+        simulation_configuration=["software", "corsika_configuration"],
+    )
+
+
+def pack_for_register(logger, simulator, args_dict):
+    """
+    Pack the output files for registering on the grid.
+
+    Parameters
+    ----------
+    logger: logging.Logger
+        Logger object.
+    simulator: Simulator
+        Simulator object.
+
+    """
+    logger.info("Packing the output files for registering on the grid")
+    output_files = simulator.get_file_list(file_type="output")
+    log_files = simulator.get_file_list(file_type="log")
+    histogram_files = simulator.get_file_list(file_type="hist")
+    tar_file_name = Path(log_files[0]).name.replace("log.gz", "log_hist.tar.gz")
+
+    with tarfile.open(tar_file_name, "w:gz") as tar:
+        files_to_tar = log_files[:1] + histogram_files[:1]
+        for file_to_tar in files_to_tar:
+            tar.add(file_to_tar, arcname=Path(file_to_tar).name)
+    directory_for_grid_upload = Path(args_dict.get("output_path")).joinpath(
+        "directory_for_grid_upload"
+    )
+    directory_for_grid_upload.mkdir(parents=True, exist_ok=True)
+    for file_to_move in [*output_files, tar_file_name]:
+        source_file = Path(file_to_move)
+        destination_file = directory_for_grid_upload / source_file.name
+        # Note that this will overwrite previous files which exist in the directory
+        # It should be fine for normal production since each run is on a separate node
+        # so no files are expected there.
+        shutil.move(source_file, destination_file)
+    logger.info(f"Output files for the grid placed in {directory_for_grid_upload!s}")
 
 
 def main():  # noqa: D103
@@ -221,72 +159,25 @@ def main():  # noqa: D103
     logger = logging.getLogger()
     logger.setLevel(gen.get_log_level_from_user(args_dict["log_level"]))
 
-    try:
-        config_data = gen.collect_data_from_file_or_dict(
-            file_name=args_dict["production_config"], in_dict=None
-        )
-    except FileNotFoundError:
-        logger.error(
-            f"Error loading simulation configuration file from {args_dict['production_config']}"
-        )
-        raise
-
-    # Overwrite default and optional settings
-    config_data["showers"]["run_list"] = args_dict["run"] + args_dict["start_run"]
-    config_data["showers"]["primary"] = args_dict["primary"]
-    config_data["common"]["site"] = args_dict["site"]
-    config_data["common"]["zenith"] = args_dict["zenith_angle"]
-    config_data["common"]["phi"] = args_dict["azimuth_angle"]
-    label = config_data["common"].pop("label", "test-production")
-    config_data["common"]["data_directory"] = Path(args_dict["data_directory"]) / label
-
-    if args_dict["nshow"] is not None:
-        config_data["showers"]["nshow"] = args_dict["nshow"]
-    if args_dict["label"] is not None:
-        label = args_dict["label"]
-
     simulator = Simulator(
-        label=label,
-        simulation_software=args_dict["simulation_software"],
-        simulator_source_path=args_dict["simtel_path"],
-        config_data=config_data,
+        label=args_dict.get("label"),
+        args_dict=args_dict,
         submit_command="local",
         test=args_dict["test"],
         mongo_db_config=db_config,
-        model_version=args_dict["model_version"],
     )
 
     simulator.simulate()
 
     logger.info(
-        f"Production run is complete for primary {config_data['showers']['primary']} showers "
-        f"coming from {config_data['common']['phi']} azimuth and zenith angle of "
-        f"{config_data['common']['zenith']} at the {args_dict['site']} site, "
+        f"Production run is complete for primary {args_dict['primary']} showers "
+        f"coming from {args_dict['azimuth_angle']} azimuth and zenith angle of "
+        f"{args_dict['zenith_angle']} at the {args_dict['site']} site, "
         f"using the {args_dict['model_version']} simulation model."
     )
 
     if args_dict["pack_for_grid_register"]:
-        logger.info("Packing the output files for registering on the grid")
-        output_files = simulator.get_list_of_output_files()
-        log_files = simulator.get_list_of_log_files()
-        histogram_files = simulator.get_list_of_histogram_files()
-        tar_file_name = Path(log_files[0]).name.replace("log.gz", "log_hist.tar.gz")
-        with tarfile.open(tar_file_name, "w:gz") as tar:
-            files_to_tar = log_files[:1] + histogram_files[:1]
-            for file_to_tar in files_to_tar:
-                tar.add(file_to_tar, arcname=Path(file_to_tar).name)
-        directory_for_grid_upload = Path(args_dict.get("output_path")).joinpath(
-            "directory_for_grid_upload"
-        )
-        directory_for_grid_upload.mkdir(parents=True, exist_ok=True)
-        for file_to_move in [*output_files, tar_file_name]:
-            source_file = Path(file_to_move)
-            destination_file = directory_for_grid_upload / source_file.name
-            # Note that this will overwrite previous files which exist in the directory
-            # It should be fine for normal production since each run is on a separate node
-            # so no files are expected there.
-            shutil.move(source_file, destination_file)
-        logger.info(f"Output files for the grid placed in {directory_for_grid_upload!s}")
+        pack_for_register(logger, simulator, args_dict)
 
 
 if __name__ == "__main__":
diff --git a/simtools/applications/simulate_showers_for_trigger_rates.py b/simtools/applications/simulate_showers_for_trigger_rates.py
deleted file mode 100644
index af0a63928..000000000
--- a/simtools/applications/simulate_showers_for_trigger_rates.py
+++ /dev/null
@@ -1,232 +0,0 @@
-#!/usr/bin/python3
-r"""
-    Simulates showers to be used in trigger rate calculations.
-
-    Arrays with one or four telescopes can be used, in case of \
-    mono or stereo trigger configurations, respectively.
-
-    Simulations are managed by the Simulator module.
-    Each run is simulated in a job. Each job is submitted by using the submission \
-    command from the global config settings. The config entry extra_commands can be used \
-    to extra commands to be ran in each job, before the actual simulation.
-
-    At the moment, the shower simulations are performed by CORSIKA, which requires \
-    the zstd package. Please, make sure that the command to set your zstd path is \
-    properly set by the extra_commands in the command line configuration.
-
-    Command line arguments
-    ----------------------
-    array_layout_name (str, required)
-        Name of the array (pre-defined in the site model).
-    site (str, required)
-        South or North.
-    primary (str, required)
-        Name of the primary particle (proton, helium ...).
-    nruns (int, optional)
-        Number of runs to be simulated.
-    nevents (int, optional)
-        Number of events simulated per run.
-    zenith (float, optional)
-        Zenith angle in deg.
-    azimuth (float, optional)
-        Azimuth angle in deg.
-    view_cone (float, optional)
-        View cone in deg.
-    scatter_x (float, optional)
-        Scatter distance (X axis) in m.
-    num_use (int, optional)
-        Number of use for each shower.
-    energy_min (float, optional)
-        Energy threshold (TeV).
-    energy_max (float, optional)
-        Maximum energy (TeV).
-    e_slope (int, optional)
-        Energy slope (spectral index).
-    data_directory (str, optional)
-        The location of the output directories corsika-data.
-        the label is added to the data_directory, such that the output
-        will be written to data_directory/label/corsika-data.
-    verbosity (str, optional)
-        Log level to print.
-
-    Example
-    -------
-    Producing a set of proton showers for trigger rate simulations of LST.
-
-    .. code-block:: console
-
-        simtools-simulate-showers-for-trigger-rates --array 4LST --site North --primary \\
-        proton --nruns 2 --nevents 10000 --submit_command local
-
-    The output is saved in simtools-output/simulate_showers_for_trigger_rates.
-
-    Expected final print-out message:
-
-    .. code-block:: console
-
-        INFO::simulate_showers_for_trigger_rates(l174)::main::List of log files exported to \
-        /workdir/external/simtools/simtools-output/simulate_showers_for_trigger_rates/application-\
-        plots/log_files_proton.list
-        INFO::simulator(l646)::get_list_of_log_files::Getting list of log files
-
-"""
-
-import logging
-from pathlib import Path
-
-import astropy.units as u
-
-from simtools.configuration import configurator
-from simtools.io_operations import io_handler
-from simtools.simulator import Simulator
-from simtools.utils import general as gen
-
-
-def _parse(label=None, description=None):
-    """
-    Parse command line configuration.
-
-    Parameters
-    ----------
-    label: str
-        application label.
-    description: str
-        description of application.
-
-    Returns
-    -------
-    CommandLineParser
-        command line parser object
-
-    """
-    config = configurator.Configurator(label=label, description=description)
-    config.parser.add_argument(
-        "--array_layout_name",
-        help="Name of the (pre-defined) array",
-        type=str,
-        required=True,
-    )
-    config.parser.add_argument(
-        "--primary",
-        help="Name of the primary particle (e.g. proton, helium ...)",
-        type=str,
-        required=True,
-    )
-    config.parser.add_argument("--nruns", help="Number of runs", type=int, default=100)
-    config.parser.add_argument(
-        "--run_number", help="Run number of the starting run", type=int, default=1
-    )
-    config.parser.add_argument("--nevents", help="Number of events/run", type=int, default=100000)
-    config.parser.add_argument("--zenith", help="Zenith angle in deg", type=float, default=20)
-    config.parser.add_argument("--azimuth", help="Azimuth angle in deg", type=float, default=0)
-    config.parser.add_argument("--view_cone", help="View cone in deg", type=float, default=10)
-    config.parser.add_argument(
-        "--scatter_x", help="Scatter distance (X axis) in m", type=float, default=1500
-    )
-
-    config.parser.add_argument(
-        "--num_use", help="Number of use for each shower", type=int, default=10
-    )
-    config.parser.add_argument(
-        "--energy_min", help="Energy threshold (TeV)", type=float, default=0.01
-    )
-    config.parser.add_argument("--energy_max", help="Maximum energy (TeV)", type=float, default=300)
-    config.parser.add_argument(
-        "--e_slope", help="Energy slope (spectral index)", type=float, default=-2
-    )
-    config.parser.add_argument(
-        "--data_directory",
-        help=(
-            "The directory where to save the corsika-data output directories."
-            "the label is added to the data_directory, such that the output"
-            "will be written to data_directory/label/corsika-data."
-        ),
-        type=str.lower,
-        required=False,
-        default="./simtools-output/",
-    )
-    return config.initialize(simulation_model="telescope", job_submission=True, db_config=True)
-
-
-def print_list_into_file(list_of_files, file_name):
-    """
-    Print the list of output files from the simulation into a log file.
-
-    Parameters
-    ----------
-    list_of_files: list
-        list of files to be printed out.
-    file_name: str
-        name of the output file.
-    """
-    with open(file_name, "w", encoding="utf-8") as f:
-        for line in list_of_files:
-            f.write(line + "\n")
-
-
-def main():  # noqa: D103
-    label = Path(__file__).stem
-    args_dict, db_config = _parse(
-        label=label, description="Simulate showers to be used for trigger rate calculations"
-    )
-
-    logger = logging.getLogger()
-    logger.setLevel(gen.get_log_level_from_user(args_dict["log_level"]))
-
-    # Output directory to save files related directly to this app
-    _io_handler = io_handler.IOHandler()
-    output_dir = _io_handler.get_output_directory(label, sub_dir="application-plots")
-    shower_config_data = {
-        "common": {
-            "data_directory": Path(args_dict["data_directory"]) / label,
-            "site": args_dict["site"],
-            "layout_name": args_dict["array_layout_name"],
-            "run_range": [
-                args_dict["run_number"],
-                args_dict["run_number"] + args_dict["nruns"] - 1,
-            ],
-            "nshow": args_dict["nevents"],
-            "primary": args_dict["primary"],
-            "zenith": args_dict["zenith"] * u.deg,
-            "azimuth": args_dict["azimuth"] * u.deg,
-        },
-        "showers": {
-            "erange": [args_dict["energy_min"] * u.TeV, args_dict["energy_max"] * u.TeV],
-            "eslope": args_dict["e_slope"],
-            "viewcone": args_dict["view_cone"] * u.deg,
-            "cscat": [
-                args_dict["num_use"],
-                args_dict["scatter_x"] * u.m,
-                0 * u.m,
-            ],
-        },
-    }
-
-    shower_simulator = Simulator(
-        label=label,
-        simulation_software="corsika_simtel",
-        simulator_source_path=args_dict.get("simtel_path", None),
-        config_data=shower_config_data,
-        submit_command=args_dict.get("submit_command", ""),
-        mongo_db_config=db_config,
-        model_version=args_dict.get("model_version", None),
-    )
-    if shower_simulator.array_model.number_of_telescopes == 1:
-        shower_simulator.array_model.site_model.change_parameter(
-            "array_triggers", "array_trigger_1MST_lapalma.dat"
-        )
-
-    shower_simulator.simulate()
-
-    # Exporting the list of output/log/input files into the application folder
-    output_file_list = output_dir.joinpath(f"output_files_{args_dict['primary']}.list")
-    log_file_list = output_dir.joinpath(f"log_files_{args_dict['primary']}.list")
-
-    logger.info(f"List of output files exported to {output_file_list}")
-    print_list_into_file(shower_simulator.get_list_of_output_files(), output_file_list)
-    logger.info(f"List of log files exported to {log_file_list}")
-    print_list_into_file(shower_simulator.get_list_of_log_files(), log_file_list)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/simtools/applications/validate_camera_efficiency.py b/simtools/applications/validate_camera_efficiency.py
index fcad20ecb..bc65b3501 100644
--- a/simtools/applications/validate_camera_efficiency.py
+++ b/simtools/applications/validate_camera_efficiency.py
@@ -161,7 +161,7 @@ def main():  # noqa: D103
             "nsb_spectrum": args_dict["nsb_spectrum"],
         },
     )
-    ce.simulate(force=True)
+    ce.simulate()
     ce.analyze(force=True)
 
     # Plotting the camera efficiency for Cherenkov light
diff --git a/simtools/camera_efficiency.py b/simtools/camera_efficiency.py
index 447b71211..e9ec2e7c1 100644
--- a/simtools/camera_efficiency.py
+++ b/simtools/camera_efficiency.py
@@ -155,15 +155,8 @@ def _load_files(self):
             )
             self._file[label] = self._base_directory.joinpath(file_name)
 
-    def simulate(self, force=False):
-        """
-        Simulate camera efficiency using testeff.
-
-        Parameters
-        ----------
-        force: bool
-            Force flag will remove existing files and simulate again.
-        """
+    def simulate(self):
+        """Simulate camera efficiency using testeff."""
         self._logger.info("Simulating CameraEfficiency")
 
         simtel = SimulatorCameraEfficiency(
@@ -175,7 +168,7 @@ def simulate(self, force=False):
             label=self.label,
             nsb_spectrum=self.config.nsb_spectrum,
         )
-        simtel.run(test=self.test, force=force)
+        simtel.run(test=self.test)
 
     def analyze(self, export=True, force=False):
         """
diff --git a/simtools/corsika/corsika_config.py b/simtools/corsika/corsika_config.py
index 3baa5cff3..92061fa8a 100644
--- a/simtools/corsika/corsika_config.py
+++ b/simtools/corsika/corsika_config.py
@@ -1,107 +1,59 @@
-"""Configuration of CORSIKA simulations."""
+"""CORSIKA configuration."""
 
-import copy
 import logging
 from pathlib import Path
 
-import astropy.units as u
 import numpy as np
 
 import simtools.utils.general as gen
 from simtools.io_operations import io_handler
-from simtools.utils.general import collect_data_from_file_or_dict
 
 __all__ = [
     "CorsikaConfig",
-    "MissingRequiredInputInCorsikaConfigDataError",
     "InvalidCorsikaInputError",
 ]
 
 
-class MissingRequiredInputInCorsikaConfigDataError(Exception):
-    """Exception for missing required input in corsika config data."""
-
-
 class InvalidCorsikaInputError(Exception):
     """Exception for invalid corsika input."""
 
 
 class CorsikaConfig:
     """
-    Configuration of CORSIKA simulations.
-
-    User parameters must be given by  the corsika_config_data or corsika_config_file arguments.
-    An example of corsika_config_data follows below.
-
-    .. code-block:: python
-
-        corsika_config_data = {
-            'data_directory': .
-            'primary': 'proton',
-            'nshow': 10000,
-            'nrun': 1,
-            'zenith': 20 * u.deg,
-            'viewcone': 5 * u.deg,
-            'erange': [10 * u.GeV, 100 * u.TeV],
-            'eslope': -2,
-            'phi': 0 * u.deg,
-            'cscat': [10, 1500 * u.m, 0]
-        }
+    Configuration for the CORSIKA air shower simulation software.
+
+    Follows closely the CORSIKA definitions and output format (see CORSIKA manual).
+
+    The configuration is set as a dict corresponding to the command line configuration groups
+    (especially simulation_software, simulation configuration, simulation parameters).
 
     Parameters
     ----------
-    array_model: ArrayModel
+    array_model : ArrayModel
         Array model.
-    label: str
+    label : str
         Instance label.
-    corsika_config_data: dict
-        CORSIKA user parameters.
-    corsika_config_file: str
-        Name of the yaml configuration file. If not provided, \
-        data/parameters/corsika_parameters.yml will be used.
-    corsika_parameters_file: str
-        Name of the yaml file to set remaining CORSIKA parameters.
-    simtel_path: str or Path
-        Location of source of the sim_telarray/CORSIKA package.
+    args_dict : dict
+        Configuration dictionary.
     """
 
-    def __init__(
-        self,
-        array_model,
-        label=None,
-        corsika_config_data=None,
-        corsika_config_file=None,
-        corsika_parameters_file=None,
-        simtel_path=None,
-    ):
+    def __init__(self, array_model, args_dict, label=None):
         """Initialize CorsikaConfig."""
         self._logger = logging.getLogger(__name__)
         self._logger.debug("Init CorsikaConfig")
 
         self.label = label
         self.primary = None
-        self.eslope = None
+        self.zenith_angle = None
+        self.azimuth_angle = None
+        self._run_number = None
+        self.args_dict = args_dict
         self.config_file_path = None
-        self._output_generic_file_name = None
-        self._simtel_path = simtel_path
 
         self.io_handler = io_handler.IOHandler()
-
         self.array_model = array_model
-        self._logger.debug(f"Building ArrayLayout {self.array_model.layout_name}")
-
-        # Load parameters
-        if corsika_parameters_file is None:
-            corsika_parameters_file = self.io_handler.get_input_data_file(
-                "parameters", "corsika_parameters.yml"
-            )
-
-        self._corsika_parameters = self.load_corsika_parameters_file(corsika_parameters_file)
-
-        corsika_config_data = collect_data_from_file_or_dict(
-            corsika_config_file, corsika_config_data
-        )
-        self.set_user_parameters(corsika_config_data)
+        self._corsika_default_parameters = self._load_corsika_default_parameters_file()
+        self.config = self.setup_configuration()
         self._is_file_updated = False
 
     def __repr__(self):
@@ -112,283 +64,93 @@ def __repr__(self):
             f"layout={self.array_model.layout_name}, label={self.label})"
         )
 
-    @staticmethod
-    def load_corsika_parameters_file(corsika_parameters_file):
+    def _load_corsika_default_parameters_file(self):
         """
-        Load CORSIKA parameters from the provided corsika_parameters_file.
-
-        Parameters
-        ----------
-        corsika_parameters_file: str or Path
-            File with CORSIKA parameters.
+        Load CORSIKA parameters.
 
         Returns
         -------
         corsika_parameters: dict
             Dictionary with CORSIKA parameters.
-        """
-        logger = logging.getLogger(__name__)
-        logger.debug(f"Loading CORSIKA parameters from file {corsika_parameters_file}")
-        return gen.collect_data_from_file_or_dict(file_name=corsika_parameters_file, in_dict=None)
-
-    def _collect_parameters(self, corsika_config_data, user_pars):
-        """
-        Collect and validate user-provided parameters.
-
-        Parameters
-        ----------
-        corsika_config_data: dict
-            User-provided configuration data.
-        user_pars: dict
-            CORSIKA user parameters.
-        """
-        for key_args, value_args in corsika_config_data.items():
-            is_identified = False
-            for par_name, par_info in user_pars.items():
-                if key_args.upper() != par_name and key_args.upper() not in par_info["names"]:
-                    continue
-                validated_value_args = self._validate_and_convert_argument(
-                    par_name, par_info, value_args
-                )
-                self._user_parameters[par_name] = validated_value_args
-                is_identified = True
-                break  # Break the inner loop if a match is found
-            if not is_identified:
-                self._raise_invalid_input_error(key_args)
-
-    def _fill_default_parameters(self, user_pars):
-        """
-        Fill in default parameters for any missing user-provided parameters.
-
-        Parameters
-        ----------
-        user_pars: dict
-            CORSIKA user parameters.
-        """
-        for par_name, par_info in user_pars.items():
-            if par_name not in self._user_parameters:
-                if "default" in par_info.keys():
-                    validated_value = self._validate_and_convert_argument(
-                        par_name, par_info, par_info["default"]
-                    )
-                    self._user_parameters[par_name] = validated_value
-                else:
-                    self._raise_missing_required_error(par_name)
-
-    def _convert_azm_to_phip(self):
-        """Convert azimuthal angle to phi prime angle."""
-        phip = 180.0 - self._user_parameters["AZM"][0]
-        phip = phip + 360.0 if phip < 0.0 else phip
-        phip = phip - 360.0 if phip >= 360.0 else phip
-        self._user_parameters["PHIP"] = [phip, phip]
-
-    def _raise_invalid_input_error(self, key_args):
-        """
-        Raise an error for an invalid input parameter.
-
-        Parameters
-        ----------
-        key_args: str
-            Invalid input parameter name.
-        """
-        msg = f"Argument {key_args} cannot be identified."
-        self._logger.error(msg)
-        raise InvalidCorsikaInputError(msg)
 
-    def _raise_missing_required_error(self, par_name):
+        Notes
+        -----
+        TODO - will be replaced by a call to the CORSIKA configuration collection
+        in the simtools database.
         """
-        Raise an error for a missing required parameter.
-
-        Parameters
-        ----------
-        par_name: str
-            Missing parameter name.
-        """
-        msg = f"Required parameters {par_name} was not given (there may be more)."
-        self._logger.error(msg)
-        raise MissingRequiredInputInCorsikaConfigDataError(msg)
-
-    def set_user_parameters(self, corsika_config_data):
-        """
-        Set user parameters from a dict.
-
-        Parameters
-        ----------
-        corsika_config_data: dict
-            Contains the user parameters. Ex.
-
-            .. code-block:: python
-
-                corsika_config_data = {
-                    'primary': 'proton',
-                    'nshow': 10000,
-                    'nrun': 1,
-                    'zenith': 20 * u.deg,
-                    'viewcone': 5 * u.deg,
-                    'erange': [10 * u.GeV, 100 * u.TeV],
-                    'eslope': -2,
-                    'phi': 0 * u.deg,
-                    'cscat': [10, 1500 * u.m, 0]
-                }
+        corsika_parameters_file = self.io_handler.get_input_data_file(
+            "parameters", "corsika_parameters.yml"
+        )
+        self._logger.debug(f"Reading CORSIKA default parameters from {corsika_parameters_file}")
+        return gen.collect_data_from_file_or_dict(file_name=corsika_parameters_file, in_dict=None)
 
-        Raises
-        ------
-        InvalidCorsikaInputError
-            If any parameter given as input has wrong len, unit or
-            an invalid name.
-        MissingRequiredInputInCorsikaConfigDataError
-            If any required user parameter is missing.
+    def setup_configuration(self):
         """
-        self._logger.debug("Setting user parameters from corsika_config_data")
-        self._user_parameters = {}
-        user_pars = self._corsika_parameters["USER_PARAMETERS"]
-
-        self._collect_parameters(corsika_config_data, user_pars)
-        self._fill_default_parameters(user_pars)
-
-        if "AZM" in self._user_parameters:
-            self._convert_azm_to_phip()
+        Set configuration parameters for CORSIKA and CorsikaConfig.
 
-        self._is_file_updated = False
-
-    def _fix_single_value_parameters(self, par_name, par_info, values):
-        """
-        Fix single value parameters by duplicating them if necessary.
-
-        Parameters
-        ----------
-        par_name: str
-            Name of the parameter.
-        par_info: dict
-            Parameter information.
-        values: list
-            Parameter values.
+        Convert values to CORSIKA-consistent units.
 
         Returns
         -------
-        list
-            Fixed parameter values.
-        """
-        if len(values) == 1 and par_name in ["THETAP", "AZM"]:
-            return values * 2
-        if len(values) == 1 and par_name == "VIEWCONE":
-            return [0.0 * u.Unit(par_info["unit"][0]), values[0]]
-        return values
-
-    def _handle_special_parameters(self, par_name, values):
-        """
-        Handle special parameters with specific processing.
-
-        Parameters
-        ----------
-        par_name: str
-            Name of the parameter.
-        values: list
-            Parameter values.
-
-        Returns
-        -------
-        list
-            Processed parameter values.
+        dict
+            Dictionary with CORSIKA parameters.
         """
-        if par_name == "PRMPAR":
-            return self._convert_primary_input_and_store_primary_name(values)
-        if par_name == "ESLOPE":
-            self.eslope = values[0]
-        return values
-
-    def _validate_length(self, par_name, par_info, values):
-        if len(values) != par_info["len"]:
-            msg = f"CORSIKA input entry with wrong len: {par_name}"
-            self._logger.error(msg)
-            raise InvalidCorsikaInputError(msg)
+        if self.args_dict is None:
+            return None
+        self._logger.debug("Setting CORSIKA parameters ")
+        self._is_file_updated = False
+        self.azimuth_angle = int(self.args_dict["azimuth_angle"].to("deg").value)
+        self.zenith_angle = self.args_dict["zenith_angle"].to("deg").value
+
+        return {
+            "EVTNR": [self.args_dict["event_number_first_shower"]],
+            "NSHOW": [self.args_dict["nshow"]],
+            "PRMPAR": [
+                self._convert_primary_input_and_store_primary_name(self.args_dict["primary"])
+            ],
+            "ESLOPE": [self.args_dict["eslope"]],
+            "ERANGE": [
+                self.args_dict["erange"][0].to("GeV").value,
+                self.args_dict["erange"][1].to("GeV").value,
+            ],
+            "THETAP": [
+                float(self.args_dict["zenith_angle"].to("deg").value),
+                float(self.args_dict["zenith_angle"].to("deg").value),
+            ],
+            "PHIP": [
+                self._rotate_azimuth_by_180deg(self.args_dict["azimuth_angle"].to("deg").value),
+                self._rotate_azimuth_by_180deg(self.args_dict["azimuth_angle"].to("deg").value),
+            ],
+            "VIEWCONE": [
+                self.args_dict["viewcone"][0].to("deg").value,
+                self.args_dict["viewcone"][1].to("deg").value,
+            ],
+            "CSCAT": [
+                self.args_dict["core_scatter"][0],
+                self.args_dict["core_scatter"][1].to("cm").value,
+                0.0,
+            ],
+        }
 
-    def _convert_units(self, par_name, values, units):
+    def _rotate_azimuth_by_180deg(self, az):
         """
-        Convert a list of values to the specified units.
+        Convert azimuth angle to the CORSIKA coordinate system.
 
         Parameters
         ----------
-        par_name: str
-            Name of the parameter as used in the CORSIKA input file.
-        values: list
-            List of values to be converted.
-        units: list
-            List of units to which the values should be converted.
+        az: float
+            Azimuth angle in degrees.
 
         Returns
         -------
-        list
-            List of values converted to the specified units.
-
-        Raises
-        ------
-        InvalidCorsikaInputError
-            If the unit conversion is not possible.
+        float
+            Azimuth angle in degrees in the CORSIKA coordinate system.
         """
-        result = []
-        for value, unit in zip(values, units):
-            if unit is None:
-                result.append(value)
-                continue
-            value = self._convert_to_quantity(value)
-            if not value.unit.is_equivalent(unit):
-                msg = f"CORSIKA input given with wrong unit: {par_name}"
-                self._logger.error(msg)
-                raise InvalidCorsikaInputError(msg)
-            result.append(value.to(unit).value)
-        return result
-
-    def _convert_to_quantity(self, value):
-        if isinstance(value, str):
-            value = u.Quantity(value)
-        if not isinstance(value, u.Quantity):
-            msg = "CORSIKA input given without unit"
-            self._logger.error(msg)
-            raise InvalidCorsikaInputError(msg)
-        return value
-
-    def _validate_and_convert_argument(self, par_name, par_info, value_args_in):
-        """
-        Validate input user parameter and convert it to the right units, if needed.
-
-        Returns the validated arguments in a list.
-
-        Parameters
-        ----------
-        par_name: str
-            Name of the parameter as used in the CORSIKA input file (e.g. PRMPAR, THETAP ...).
-
-        par_info: dict
-            Dictionary with parameter data.
-
-        value_args_in: list
-            List of values for the parameter.
-
-        Returns
-        -------
-        any
-            Validated and converted parameter value.
-
-        Raises
-        ------
-        InvalidCorsikaInputError
-            If the input value has the wrong length or unit.
-        """
-        value_args = self._convert_to_quantities(value_args_in)
-        value_args = self._fix_single_value_parameters(par_name, par_info, value_args)
-        value_args = self._handle_special_parameters(par_name, value_args)
-        self._validate_length(par_name, par_info, value_args)
-
-        if "unit" not in par_info:
-            return value_args
-
-        par_unit = gen.copy_as_list(par_info["unit"])
-        return self._convert_units(par_name, value_args, par_unit)
+        return (az + 180) % 360
 
     def _convert_primary_input_and_store_primary_name(self, value):
         """
-        Convert a primary name into the proper CORSIKA particle ID.
+        Convert a primary name into the CORSIKA particle ID.
 
         Parameters
         ----------
@@ -404,23 +166,27 @@ def _convert_primary_input_and_store_primary_name(self, value):
         -------
         int
             Respective number of the given primary.
+
+        Notes
+        -----
+        TODO - this will be replaced using the 'particle' PDG package.
         """
-        for prim_name, prim_info in self._corsika_parameters["PRIMARIES"].items():
-            if value[0].upper() == prim_name or value[0].upper() in prim_info["names"]:
+        for prim_name, prim_info in self._corsika_default_parameters["PRIMARIES"].items():
+            if value.upper() == prim_name or value.upper() in prim_info["names"]:
                 self.primary = prim_name.lower()
-                return [prim_info["number"]]
+                return prim_info["number"]
         msg = f"Primary not valid: {value}"
         self._logger.error(msg)
         raise InvalidCorsikaInputError(msg)
 
-    def get_user_parameter(self, par_name):
+    def get_config_parameter(self, par_name):
         """
-        Get the value of a user parameter.
+        Get value of CORSIKA configuration parameter.
 
         Parameters
         ----------
         par_name: str
-            Name of the parameter as used in the CORSIKA input file (e.g. PRMPAR, THETAP ...)
+            Name of the parameter as used in the CORSIKA input file (e.g. PRMPAR, THETAP ...).
 
         Raises
         ------
@@ -433,92 +199,96 @@ def get_user_parameter(self, par_name):
             Value(s) of the parameter.
         """
         try:
-            par_value = self._user_parameters[par_name.upper()]
-        except KeyError:
-            self._logger.warning(f"Parameter {par_name} is not a user parameter")
-            raise
-
+            par_value = self.config[par_name]
+        except KeyError as exc:
+            self._logger.error(f"Parameter {par_name} is not a CORSIKA config parameter")
+            raise exc
         return par_value if len(par_value) > 1 else par_value[0]
 
-    def print_user_parameters(self):
-        """Print user parameters for inspection."""
-        for par, value in self._user_parameters.items():
+    def print_config_parameter(self):
+        """Print CORSIKA config parameters for inspection."""
+        for par, value in self.config.items():
             print(f"{par} = {value}")
 
-    def export_input_file(self, use_multipipe=False):
+    @staticmethod
+    def _get_text_single_line(pars, line_begin=""):
+        """
+        Return one parameter per line for each input parameter.
+
+        Parameters
+        ----------
+        pars: dict
+            Dictionary with the parameters to be written in the file.
+
+        Returns
+        -------
+        str
+            Text with the parameters.
+        """
+        text = ""
+        for par, values in pars.items():
+            line = line_begin + par + " "
+            for v in values:
+                line += str(v) + " "
+            line += "\n"
+            text += line
+        return text
+
+    def generate_corsika_input_file(self, use_multipipe=False):
         """
-        Create and export CORSIKA input file.
+        Generate a CORSIKA input file.
 
         Parameters
         ----------
         use_multipipe: bool
             Whether to set the CORSIKA Inputs file to pipe
-            the output directly to sim_telarray or not.
+            the output directly to sim_telarray.
+
         """
-        sub_dir = "corsika_simtel" if use_multipipe else "corsika"
-        self._set_output_file_and_directory(sub_dir)
-        self._logger.debug(f"Exporting CORSIKA input file to {self.config_file_path}")
-
-        def _get_text_single_line(pars):
-            text = ""
-            for par, values in pars.items():
-                line = par + " "
-                for v in values:
-                    line += str(v) + " "
-                line += "\n"
-                text += line
-            return text
-
-        def _get_text_multiple_lines(pars):
-            text = ""
-            for par, value_list in pars.items():
-                for value in value_list:
-                    new_pars = {par: value}
-                    text += _get_text_single_line(new_pars)
-            return text
+        if self._is_file_updated:
+            self._logger.debug(f"CORSIKA input file already updated: {self.config_file_path}")
+            return self.config_file_path
+        _output_generic_file_name = self.set_output_file_and_directory(use_multipipe=use_multipipe)
+        self._logger.info(f"Exporting CORSIKA input file to {self.config_file_path}")
 
         with open(self.config_file_path, "w", encoding="utf-8") as file:
             file.write("\n* [ RUN PARAMETERS ]\n")
-            # Removing AZM entry first
-            _user_pars_temp = copy.copy(self._user_parameters)
-            _user_pars_temp.pop("AZM")
-            text_parameters = _get_text_single_line(_user_pars_temp)
+            text_parameters = self._get_text_single_line(self.config)
             file.write(text_parameters)
 
             file.write("\n* [ SITE PARAMETERS ]\n")
-            text_site_parameters = _get_text_single_line(
+            text_site_parameters = self._get_text_single_line(
                 self.array_model.site_model.get_corsika_site_parameters(config_file_style=True)
             )
             file.write(text_site_parameters)
 
-            # Defining the IACT variables for the output file name
-            file.write("\n")
+            file.write("\n* [ IACT ENV PARAMETERS ]\n")
             file.write(f"IACT setenv PRMNAME {self.primary}\n")
-            file.write(f"IACT setenv ZA {int(self._user_parameters['THETAP'][0])}\n")
-            file.write(f"IACT setenv AZM {int(self._user_parameters['AZM'][0])}\n")
+            file.write(f"IACT setenv ZA {int(self.config['THETAP'][0])}\n")
+            file.write(f"IACT setenv AZM {self.azimuth_angle}\n")
 
             file.write("\n* [ SEEDS ]\n")
             self._write_seeds(file)
 
             file.write("\n* [ TELESCOPES ]\n")
-            telescope_list_text = self.get_corsika_input_list()
+            telescope_list_text = self.get_corsika_telescope_list()
             file.write(telescope_list_text)
 
             file.write("\n* [ INTERACTION FLAGS ]\n")
-            text_interaction_flags = _get_text_single_line(
-                self._corsika_parameters["INTERACTION_FLAGS"]
+            text_interaction_flags = self._get_text_single_line(
+                self._corsika_default_parameters["INTERACTION_FLAGS"]
             )
             file.write(text_interaction_flags)
 
             file.write("\n* [ CHERENKOV EMISSION PARAMETERS ]\n")
-            text_cherenkov = _get_text_single_line(
-                self._corsika_parameters["CHERENKOV_EMISSION_PARAMETERS"]
+            text_cherenkov = self._get_text_single_line(
+                self._corsika_default_parameters["CHERENKOV_EMISSION_PARAMETERS"]
             )
             file.write(text_cherenkov)
 
             file.write("\n* [ DEBUGGING OUTPUT PARAMETERS ]\n")
-            text_debugging = _get_text_single_line(
-                self._corsika_parameters["DEBUGGING_OUTPUT_PARAMETERS"]
+            text_debugging = self._get_text_single_line(
+                self._corsika_default_parameters["DEBUGGING_OUTPUT_PARAMETERS"]
             )
             file.write(text_debugging)
 
@@ -527,19 +297,22 @@ def _get_text_multiple_lines(pars):
                 run_cta_script = Path(self.config_file_path.parent).joinpath("run_cta_multipipe")
                 file.write(f"TELFIL |{run_cta_script!s}\n")
             else:
-                file.write(f"TELFIL {self._output_generic_file_name}\n")
+                file.write(f"TELFIL {_output_generic_file_name}\n")
 
             file.write("\n* [ IACT TUNING PARAMETERS ]\n")
-            text_iact = _get_text_multiple_lines(self._corsika_parameters["IACT_TUNING_PARAMETERS"])
+            text_iact = self._get_text_single_line(
+                self._corsika_default_parameters["IACT_TUNING_PARAMETERS"],
+                "IACT ",
+            )
             file.write(text_iact)
-
             file.write("\nEXIT")
 
         self._is_file_updated = True
+        return self.config_file_path
 
-    def get_file_name(self, file_type, run_number=None):
+    def get_corsika_config_file_name(self, file_type, run_number=None):
         """
-        Get a CORSIKA config style file name for various file types.
+        Get a CORSIKA config style file name for various configuration file types.
 
         Parameters
         ----------
@@ -553,14 +326,14 @@ def get_file_name(self, file_type, run_number=None):
         -------
         str
             for file_type="config_tmp":
-                Get the CORSIKA input file for one specific run.
+                Return CORSIKA input file name for one specific run.
                 This is the input file after being pre-processed by sim_telarray (pfp).
             for file_type="config":
-                Get a general CORSIKA config inputs file.
+                Return generic CORSIKA config input file name.
             for file_type="output_generic"
-                Get a generic file name for the TELFIL option in the CORSIKA inputs file.
+                Return generic file name for the TELFIL option in the CORSIKA inputs file.
             for file_type="multipipe"
-                Get a multipipe "file name" for the TELFIL option in the CORSIKA inputs file.
+                Return multipipe "file name" for the TELFIL option in the CORSIKA inputs file.
 
         Raises
         ------
@@ -568,46 +341,60 @@ def get_file_name(self, file_type, run_number=None):
             If file_type is unknown or if the run number is not given for file_type==config_tmp.
         """
         file_label = f"_{self.label}" if self.label is not None else ""
-        view_cone = ""
-        if self._user_parameters["VIEWCONE"][0] != 0 or self._user_parameters["VIEWCONE"][1] != 0:
-            view_cone = (
-                f"_cone{int(self._user_parameters['VIEWCONE'][0]):d}-"
-                f"{int(self._user_parameters['VIEWCONE'][1]):d}"
-            )
-        file_name = (
+
+        view_cone = (
+            f"_cone{int(self.config['VIEWCONE'][0]):d}-{int(self.config['VIEWCONE'][1]):d}"
+            if self.config["VIEWCONE"][0] != 0 or self.config["VIEWCONE"][1] != 0
+            else ""
+        )
+
+        base_name = (
             f"{self.primary}_{self.array_model.site}_{self.array_model.layout_name}_"
-            f"za{int(self._user_parameters['THETAP'][0]):03}-"
-            f"azm{int(self._user_parameters['AZM'][0]):03}deg"
+            f"za{int(self.config['THETAP'][0]):03}-"
+            f"azm{self.azimuth_angle:03}deg"
             f"{view_cone}{file_label}"
         )
 
         if file_type == "config_tmp":
-            if run_number is not None:
-                return f"corsika_config_run{run_number:06}_{file_name}.txt"
-            raise ValueError("Must provide a run number for a temporary CORSIKA config file")
+            if run_number is None:
+                raise ValueError("Must provide a run number for a temporary CORSIKA config file")
+            return f"corsika_config_run{run_number:06}_{base_name}.txt"
         if file_type == "config":
-            return f"corsika_config_{file_name}.input"
+            return f"corsika_config_{base_name}.input"
         if file_type == "output_generic":
             # The XXXXXX will be replaced by the run number after the pfp step with sed
             return (
-                f"corsika_runXXXXXX_"
-                f"{self.primary}_za{int(self._user_parameters['THETAP'][0]):03}deg_"
-                f"azm{int(self._user_parameters['AZM'][0]):03}deg"
-                f"_{self.array_model.site}_{self.array_model.layout_name}{file_label}.zst"
+                f"runXXXXXX_{base_name}_{self.array_model.site}_"
+                f"{self.array_model.layout_name}{file_label}.zst"
             )
         if file_type == "multipipe":
             return f"multi_cta-{self.array_model.site}-{self.array_model.layout_name}.cfg"
 
         raise ValueError(f"The requested file type ({file_type}) is unknown")
 
-    def _set_output_file_and_directory(self, sub_dir="corsika"):
-        config_file_name = self.get_file_name(file_type="config")
+    def set_output_file_and_directory(self, use_multipipe=False):
+        """
+        Set output file names and directories.
+
+        Parameters
+        ----------
+        use_multipipe: bool
+            Whether to set the CORSIKA Inputs file to pipe
+            the output directly to sim_telarray. Defines directory names.
+
+        Returns
+        -------
+        str
+            Output file name.
+        """
+        sub_dir = "corsika_simtel" if use_multipipe else "corsika"
+        config_file_name = self.get_corsika_config_file_name(file_type="config")
         file_directory = self.io_handler.get_output_directory(label=self.label, sub_dir=sub_dir)
-        self._logger.info(f"Creating directory {file_directory}, if needed.")
+        self._logger.debug(f"Creating directory {file_directory}")
         file_directory.mkdir(parents=True, exist_ok=True)
         self.config_file_path = file_directory.joinpath(config_file_name)
 
-        self._output_generic_file_name = self.get_file_name(file_type="output_generic")
+        return self.get_corsika_config_file_name(file_type="output_generic")
 
     def _write_seeds(self, file):
         """
@@ -618,64 +405,13 @@ def _write_seeds(self, file):
         file: stream
             File where the telescope positions will be written.
         """
-        random_seed = self._user_parameters["PRMPAR"][0] + self._user_parameters["RUNNR"][0]
+        random_seed = self.config["PRMPAR"][0] + self._run_number
         rng = np.random.default_rng(random_seed)
         corsika_seeds = [int(rng.uniform(0, 1e7)) for _ in range(4)]
-
         for s in corsika_seeds:
             file.write(f"SEED {s} 0 0\n")
 
-    def get_input_file(self, use_multipipe=False):
-        """
-        Get the full path of the CORSIKA input file.
-
-        Returns
-        -------
-        Path:
-            Full path of the CORSIKA input file.
-        """
-        if not self._is_file_updated:
-            self.export_input_file(use_multipipe)
-        return self.config_file_path
-
-    def _convert_to_quantities(self, value_args):
-        """
-        Convert a list of value, unit pairs into a list of astropy quantities.
-
-        (note similarity to simtools.general.validate_config_data; unfortunately
-        minor differences are required as CORSIKA is very specific about the
-        input parameter representation).
-
-        Parameters
-        ----------
-        value_args: list
-            List of value/unit pairs (e.g., ["10 m", "20 m"])
-
-        Returns
-        -------
-        list
-            List of astropy quantities (or strings)
-        """
-        if isinstance(value_args, str):
-            return [value_args]
-        if isinstance(value_args, dict) and "value" in value_args and "unit" in value_args:
-            return [value_args["value"] * u.Unit(value_args["unit"])]
-        if isinstance(value_args, list):
-            return [
-                (
-                    value
-                    if isinstance(value, u.Quantity)
-                    else (
-                        value["value"] * u.Unit(value["unit"])
-                        if isinstance(value, dict) and "value" in value and "unit" in value
-                        else value
-                    )
-                )
-                for value in value_args
-            ]
-        return [value_args]
-
-    def get_corsika_input_list(self):
+    def get_corsika_telescope_list(self):
         """
         List of telescope positions in the format required for the CORSIKA input file.
 
@@ -697,3 +433,49 @@ def get_corsika_input_list(self):
             corsika_input_list += f"\t # {telescope_name}\n"
 
         return corsika_input_list
+
+    @property
+    def run_number(self):
+        """Set run number."""
+        return self._run_number
+
+    @run_number.setter
+    def run_number(self, run_number):
+        """
+        Set run number and validate it.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+        """
+        self._run_number = self.validate_run_number(run_number)
+
+    def validate_run_number(self, run_number):
+        """
+        Validate run number and return it.
+
+        Return run number from configuration if None.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        int
+            Run number.
+
+        Raises
+        ------
+        ValueError
+            If run_number is not a valid value (e.g., < 1).
+        """
+        if run_number is None:
+            return self.run_number
+        if not float(run_number).is_integer() or run_number < 1 or run_number > 999999:
+            msg = f"Invalid type of run number ({run_number}) - it must be an uint < 1000000."
+            self._logger.error(msg)
+            raise ValueError(msg)
+        return run_number
diff --git a/simtools/corsika/corsika_default_config.py b/simtools/corsika/corsika_default_config.py
deleted file mode 100644
index bc207421d..000000000
--- a/simtools/corsika/corsika_default_config.py
+++ /dev/null
@@ -1,281 +0,0 @@
-"""Default configuration for CORSIKA parameters for the various primary particles."""
-
-from collections import defaultdict
-
-import astropy.units as u
-from scipy.interpolate import interp1d
-
-
-class CorsikaDefaultConfig:
-    """
-    Default configuration for CORSIKA parameters for the various primary particles.
-
-    It includes all basic dependencies on zenith angles, etc.
-    The default values defined in this class assume the full CTAO arrays are simulated,
-    including full CTAO energy range and number of events optimized to run for roughly 24 hours
-    on a single node on the grid.
-    """
-
-    def __init__(self, primary=None, zenith_angle=None):
-        """Initialize CorsikaDefaultConfig."""
-        self.primary = primary
-        self.zenith_angle = zenith_angle
-        self._energy_slope = -2.0
-        self.energy_ranges = self._define_hardcoded_energy_ranges()
-        self.number_of_showers = self._define_hardcoded_number_of_showers()
-
-    @property
-    def primary(self):
-        """
-        Primary particle.
-
-        Returns
-        -------
-        primary: str
-        """
-        return self._primary
-
-    @primary.setter
-    def primary(self, primary):
-        """
-        Set primary particle.
-
-        Parameters
-        ----------
-        primary: str
-            Which primary to simulate.
-        """
-        supported_primaries = [
-            "gamma",
-            "gamma_diffuse",
-            "electron",
-            "proton",
-            "muon",
-            "helium",
-            "nitrogen",
-            "silicon",
-            "iron",
-        ]
-        if primary not in supported_primaries:
-            raise ValueError(
-                f"Invalid primary particle: {primary}. Must be one of {supported_primaries}"
-            )
-        self._primary = primary
-
-    @property
-    def zenith_angle(self):
-        """
-        Zenith angle.
-
-        Returns
-        -------
-        zenith_angle: astropy.units.Quantity
-        """
-        return self._zenith_angle
-
-    @zenith_angle.setter
-    @u.quantity_input(zenith_angle=u.deg)
-    def zenith_angle(self, zenith_angle):
-        """
-        Set zenith angle.
-
-        Parameters
-        ----------
-        zenith_angle: astropy.units.Quantity
-            Which zenith angle to simulate (in degrees).
-        """
-        allowed_zenith_angle_interval = [20.0, 60.0] * u.deg
-        if (
-            zenith_angle < allowed_zenith_angle_interval[0]
-            or zenith_angle > allowed_zenith_angle_interval[1]
-        ):
-            raise ValueError(
-                f"The zenith angle, {zenith_angle:.1f}, "
-                f"is outside of the allowed interval, {allowed_zenith_angle_interval}."
-                "This interval is enforced because values can only be interpolated "
-                "between zenith angles that were manually optimized."
-            )
-
-        self._zenith_angle = zenith_angle
-
-    @property
-    def energy_slope(self):
-        """
-        Energy slope.
-
-        Returns
-        -------
-        energy_slope: float
-        """
-        return self._energy_slope
-
-    def _define_hardcoded_energy_ranges(self):
-        """
-        Define the hardcoded energy ranges for the various primaries.
-
-        These energy ranges are for the full CTAO energy range (for both sites).
-
-        Returns
-        -------
-        energy_ranges: dict
-            Dictionary with the default energy ranges for the various primaries.
-        """
-        energy_ranges = defaultdict(dict)
-        energy_ranges["gamma"][20] = [3 * u.GeV, 330 * u.TeV]
-        energy_ranges["gamma"][40] = [6 * u.GeV, 660 * u.TeV]
-        energy_ranges["gamma"][60] = [12 * u.GeV, 990 * u.TeV]
-
-        for zenith_angle in energy_ranges["gamma"]:
-            energy_ranges["gamma_diffuse"][zenith_angle] = energy_ranges["gamma"][zenith_angle]
-            energy_ranges["electron"][zenith_angle] = energy_ranges["gamma"][zenith_angle]
-
-        energy_ranges["proton"][20] = [8 * u.GeV, 600 * u.TeV]
-        energy_ranges["proton"][40] = [12 * u.GeV, 800 * u.TeV]
-        energy_ranges["proton"][60] = [16 * u.GeV, 1800 * u.TeV]
-
-        energy_ranges["helium"][20] = [10 * u.GeV, 1200 * u.TeV]
-        energy_ranges["helium"][40] = [20 * u.GeV, 2400 * u.TeV]
-        energy_ranges["helium"][60] = [40 * u.GeV, 3600 * u.TeV]
-
-        energy_ranges["nitrogen"][20] = [40 * u.GeV, 4000 * u.TeV]
-        energy_ranges["nitrogen"][40] = [80 * u.GeV, 8000 * u.TeV]
-        energy_ranges["nitrogen"][60] = [160 * u.GeV, 12000 * u.TeV]
-
-        energy_ranges["silicon"][20] = [50 * u.GeV, 5000 * u.TeV]
-        energy_ranges["silicon"][40] = [100 * u.GeV, 10000 * u.TeV]
-        energy_ranges["silicon"][60] = [200 * u.GeV, 15000 * u.TeV]
-
-        energy_ranges["iron"][20] = [60 * u.GeV, 6000 * u.TeV]
-        energy_ranges["iron"][40] = [120 * u.GeV, 12000 * u.TeV]
-        energy_ranges["iron"][60] = [240 * u.GeV, 18000 * u.TeV]
-
-        return energy_ranges
-
-    def _define_hardcoded_number_of_showers(self):
-        """
-        Define the hardcoded number of showers for the various primaries.
-
-        Returns
-        -------
-        number_of_showers: dict
-            Dictionary with the default number of showers for the various primaries.
-        """
-        number_of_showers = defaultdict(dict)
-        number_of_showers["gamma"][20] = 5000
-        number_of_showers["gamma"][40] = 5000
-        number_of_showers["gamma"][60] = 2500
-
-        for zenith_angle in number_of_showers["gamma"]:
-            number_of_showers["gamma_diffuse"][zenith_angle] = number_of_showers["gamma"][
-                zenith_angle
-            ]
-            number_of_showers["electron"][zenith_angle] = number_of_showers["gamma"][zenith_angle]
-
-        number_of_showers["proton"][20] = 15000
-        number_of_showers["proton"][40] = 15000
-        number_of_showers["proton"][60] = 7500
-
-        number_of_showers["helium"][20] = 10000
-        number_of_showers["helium"][40] = 10000
-        number_of_showers["helium"][60] = 5000
-
-        number_of_showers["nitrogen"][20] = 2000
-        number_of_showers["nitrogen"][40] = 2000
-        number_of_showers["nitrogen"][60] = 1000
-
-        number_of_showers["silicon"][20] = 1000
-        number_of_showers["silicon"][40] = 1000
-        number_of_showers["silicon"][60] = 500
-
-        return number_of_showers
-
-    @staticmethod
-    @u.quantity_input(zenith_angle=u.deg)
-    def interpolate_to_zenith_angle(
-        zenith_angle, zenith_angles_to_interpolate, values_to_interpolate
-    ):
-        """
-        Interpolate values like energy range or number of showers to the provided zenith angle.
-
-        Parameters
-        ----------
-        zenith_angle: astropy.units.Quantity
-            Which zenith angle to interpolate to (in degrees).
-        zenith_angles_to_interpolate: list
-            List of zenith angles for which we have values to interpolate between.
-        values_to_interpolate: list
-            List of values to interpolate between.
-
-        Returns
-        -------
-        float
-            Interpolated value.
-        """
-        interpolation_function = interp1d(
-            zenith_angles_to_interpolate, values_to_interpolate, kind="quadratic"
-        )
-        return interpolation_function(zenith_angle.to_value(u.deg)).item()
-
-    def energy_range_for_primary(self):
-        """
-        Get the energy range for the primary particle for the given zenith angle.
-
-        Returns
-        -------
-        energy_range: list
-            List with the energy range for the primary particle for the given zenith angle.
-        """
-        zenith_angles_to_interpolate = [*self.energy_ranges[self.primary].keys()]
-        min_energy_to_interpolate = [
-            energy[0].to_value(u.GeV) for energy in self.energy_ranges[self.primary].values()
-        ]
-        max_energy_to_interpolate = [
-            energy[1].to_value(u.GeV) for energy in self.energy_ranges[self.primary].values()
-        ]
-
-        return [
-            self.interpolate_to_zenith_angle(
-                self.zenith_angle, zenith_angles_to_interpolate, min_energy_to_interpolate
-            )
-            * u.GeV,
-            (
-                self.interpolate_to_zenith_angle(
-                    self.zenith_angle, zenith_angles_to_interpolate, max_energy_to_interpolate
-                )
-                * u.GeV
-            ).to(u.TeV),
-        ]
-
-    def number_of_showers_for_primary(self):
-        """
-        Get the number of showers for the primary particle for the given zenith angle.
-
-        Returns
-        -------
-        number_of_showers: int
-            Number of showers for the primary particle for the given zenith angle.
-        """
-        zenith_angles_to_interpolate = [*self.energy_ranges[self.primary].keys()]
-        number_of_showers = [*self.number_of_showers[self.primary].values()]
-
-        return self.interpolate_to_zenith_angle(
-            self.zenith_angle,
-            zenith_angles_to_interpolate,
-            number_of_showers,
-        )
-
-    def view_cone_for_primary(self):
-        """
-        Get the view cone for the primary particle.
-
-        All diffuse primaries have a view cone of 10 deg by default.
-
-        Returns
-        -------
-        view_cone: list
-            List with the view cone for the primary particle.
-        """
-        if self.primary == "gamma":
-            return [0.0 * u.deg, 0.0 * u.deg]
-
-        return [0 * u.deg, 10 * u.deg]
diff --git a/simtools/corsika/corsika_runner.py b/simtools/corsika/corsika_runner.py
deleted file mode 100644
index e09ccf01f..000000000
--- a/simtools/corsika/corsika_runner.py
+++ /dev/null
@@ -1,435 +0,0 @@
-"""Generate scripts for running CORSIKA air shower simulations."""
-
-import logging
-import os
-from copy import copy
-from pathlib import Path
-
-from simtools.corsika.corsika_config import (
-    CorsikaConfig,
-    MissingRequiredInputInCorsikaConfigDataError,
-)
-from simtools.io_operations import io_handler
-
-__all__ = ["CorsikaRunner", "MissingRequiredEntryInCorsikaConfigError"]
-
-
-class MissingRequiredEntryInCorsikaConfigError(Exception):
-    """Exception for missing required entry in corsika config."""
-
-
-class CorsikaRunner:
-    """
-    CorsikaRunner is responsible for running CORSIKA.
-
-    Uses the corsika_autoinputs program provided by the sim_telarray package.
-    It provides shell scripts to be run externally or by \
-    the module simulator. Same instance can be used to generate scripts for any given run number.
-
-    It uses CorsikaConfig to manage the CORSIKA configuration. User parameters must be given by the\
-    corsika_config_data or corsika_config_file arguments. An example of corsika_config_data follows\
-    below.
-
-    .. code-block:: python
-
-        corsika_config_data = {
-            'data_directory': .
-            'primary': 'proton',
-            'nshow': 10000,
-            'nrun': 1,
-            'zenith': 20 * u.deg,
-            'viewcone': 5 * u.deg,
-            'erange': [10 * u.GeV, 100 * u.TeV],
-            'eslope': -2,
-            'phi': 0 * u.deg,
-            'cscat': [10, 1500 * u.m, 0]
-        }
-
-    The remaining CORSIKA parameters can be set as a yaml file, using the argument \
-    corsika_parameters_file. When not given, corsika_parameters will be loaded from \
-    data/parameters/corsika_parameters.yml.
-
-    The CORSIKA output directory must be set by the data_directory entry. The following directories\
-    will be created to store the logs and input file:
-    {data_directory}/corsika/$site/$primary/logs
-    {data_directory}/corsika/$site/$primary/scripts
-
-    Parameters
-    ----------
-    array_model: ArrayModel
-        Array model instance.
-    simtel_path: str or Path
-        Location of source of the sim_telarray/CORSIKA package.
-    label: str
-        Instance label.
-    keep_seeds: bool
-        Use seeds based on run number and primary particle. If False, use sim_telarray seeds.
-    corsika_parameters_file: str or Path
-        Path to yaml file containing CORSIKA parameters.
-    corsika_config_data: dict
-        Dict with CORSIKA config data.
-    use_multipipe: bool
-        Use multipipe to run CORSIKA and sim_telarray.
-    """
-
-    def __init__(
-        self,
-        array_model,
-        simtel_path,
-        label=None,
-        keep_seeds=False,
-        corsika_parameters_file=None,
-        corsika_config_data=None,
-        use_multipipe=False,
-    ):
-        """CorsikaRunner init."""
-        self._logger = logging.getLogger(__name__)
-        self._logger.debug("Init CorsikaRunner")
-
-        self.label = label
-        self.array_model = array_model
-
-        self._keep_seeds = keep_seeds
-
-        self._simtel_path = Path(simtel_path)
-        self.io_handler = io_handler.IOHandler()
-        _runner_directory = "corsika_simtel" if use_multipipe else "corsika"
-        self._output_directory = self.io_handler.get_output_directory(self.label, _runner_directory)
-        self._logger.debug(f"Creating output dir {self._output_directory}, if needed,")
-
-        self._corsika_parameters_file = corsika_parameters_file
-        self._load_corsika_config_data(corsika_config_data)
-        self._define_corsika_config(use_multipipe)
-
-        self._load_corsika_data_directories()
-
-    def _load_corsika_config_data(self, corsika_config_data):
-        """Read corsika_config_data, creates corsika_config and corsika_input_file."""
-        corsika_data_directory_from_config = corsika_config_data.get("data_directory", None)
-        if corsika_data_directory_from_config is None:
-            # corsika_data_directory not given (or None).
-            msg = (
-                "data_directory not given in corsika_config "
-                "- default output directory will be set."
-            )
-            self._logger.warning(msg)
-            self._corsika_data_directory = self._output_directory
-        else:
-            # corsika_data_directory given and not None.
-            self._corsika_data_directory = Path(corsika_data_directory_from_config)
-
-        self._corsika_data_directory = self._corsika_data_directory.joinpath("corsika-data")
-
-        # Copying corsika_config_data and removing corsika_data_directory
-        # (it does not go to CorsikaConfig)
-        self._corsika_config_data = copy(corsika_config_data)
-        self._corsika_config_data.pop("data_directory", None)
-
-    def _define_corsika_config(self, use_multipipe=False):
-        """
-        Create the CORSIKA config instance.
-
-        This validates the input given in corsika_config_data as well.
-        """
-        try:
-            self.corsika_config = CorsikaConfig(
-                label=self.label,
-                array_model=self.array_model,
-                corsika_config_data=self._corsika_config_data,
-                simtel_path=self._simtel_path,
-                corsika_parameters_file=self._corsika_parameters_file,
-            )
-            # CORSIKA input file used as template for all runs
-            self._corsika_input_file = self.corsika_config.get_input_file(use_multipipe)
-        except MissingRequiredInputInCorsikaConfigDataError:
-            msg = "corsika_config_data is missing required entries."
-            self._logger.error(msg)
-            raise
-
-    def _load_corsika_data_directories(self):
-        """Create CORSIKA directories for data, log and input."""
-        corsika_base_dir = self._corsika_data_directory.joinpath(self.array_model.site)
-        corsika_base_dir = corsika_base_dir.joinpath(self.corsika_config.primary)
-        corsika_base_dir = corsika_base_dir.absolute()
-
-        self._corsika_data_dir = corsika_base_dir.joinpath("data")
-        self._corsika_data_dir.mkdir(parents=True, exist_ok=True)
-        self._corsika_input_dir = corsika_base_dir.joinpath("input")
-        self._corsika_input_dir.mkdir(parents=True, exist_ok=True)
-        self._corsika_log_dir = corsika_base_dir.joinpath("log")
-        self._corsika_log_dir.mkdir(parents=True, exist_ok=True)
-
-    def prepare_run_script(self, use_pfp=True, **kwargs):
-        """
-        Get the full path of the run script file for a given run number.
-
-        Parameters
-        ----------
-        use_pfp: bool
-            Whether to use the preprocessor in preparing the CORSIKA input file
-        kwargs: dict
-            The following optional parameters can be provided:
-                run_number: int
-                    Run number.
-                extra_commands: str
-                    Additional commands for running simulations.
-
-        Returns
-        -------
-        Path:
-            Full path of the run script file.
-        """
-        kwargs = {
-            "run_number": None,
-            "extra_commands": None,
-            **kwargs,
-        }
-        run_number = self._validate_run_number(kwargs["run_number"])
-
-        script_file_path = self.get_file_name(
-            file_type="script", **self.get_info_for_file_name(run_number)
-        )
-
-        # CORSIKA input file for a specific run, created by the preprocessor pfp
-        corsika_input_tmp_name = self.corsika_config.get_file_name(
-            file_type="config_tmp", run_number=run_number
-        )
-        corsika_input_tmp_file = self._corsika_input_dir.joinpath(corsika_input_tmp_name)
-
-        if use_pfp:
-            pfp_command = self._get_pfp_command(corsika_input_tmp_file)
-        autoinputs_command = self._get_autoinputs_command(run_number, corsika_input_tmp_file)
-
-        extra_commands = kwargs["extra_commands"]
-        self._logger.debug(f"Extra commands to be added to the run script {extra_commands}")
-
-        with open(script_file_path, "w", encoding="utf-8") as file:
-            # shebang
-            file.write("#!/usr/bin/env bash\n")
-
-            # Make sure to exit on failed commands and report their error code
-            file.write("set -e\n")
-            file.write("set -o pipefail\n")
-
-            # Setting SECONDS variable to measure runtime
-            file.write("\nSECONDS=0\n")
-
-            if extra_commands is not None:
-                file.write("\n# Writing extras\n")
-                file.write(f"{extra_commands}\n")
-                file.write("# End of extras\n\n")
-
-            file.write(f"export CORSIKA_DATA={self._corsika_data_dir}\n")
-            file.write("\n# Creating CORSIKA_DATA\n")
-            file.write(f"mkdir -p {self._corsika_data_dir}\n")
-            file.write(f"cd {self._corsika_data_dir} || exit 2\n")
-            if use_pfp:
-                file.write("\n# Running pfp\n")
-                file.write(pfp_command)
-                file.write("\n# Replacing the XXXXXX placeholder with the run number\n")
-                file.write(f"sed -i 's/XXXXXX/{run_number:06}/g' {corsika_input_tmp_file}\n")
-            else:
-                file.write("\n# Copying CORSIKA input file to run location\n")
-                file.write(f"cp {self._corsika_input_file} {corsika_input_tmp_file}")
-            file.write("\n# Running corsika_autoinputs\n")
-            file.write(autoinputs_command)
-
-            # Printing out runtime
-            file.write('\necho "RUNTIME: $SECONDS"\n')
-
-        # Changing permissions
-        os.system(f"chmod ug+x {script_file_path}")
-
-        return script_file_path
-
-    def _get_pfp_command(self, input_tmp_file):
-        """Get pfp pre-processor command."""
-        cmd = self._simtel_path.joinpath("sim_telarray/bin/pfp")
-        cmd = str(cmd) + f" -V -DWITHOUT_MULTIPIPE - < {self._corsika_input_file}"
-        cmd += f" > {input_tmp_file} || exit\n"
-        return cmd
-
-    def _get_autoinputs_command(self, run_number, input_tmp_file):
-        """Get autoinputs command."""
-        corsika_bin_path = self._simtel_path.joinpath("corsika-run/corsika")
-
-        log_file = self.get_file_name(
-            file_type="corsika_autoinputs_log", **self.get_info_for_file_name(run_number)
-        )
-
-        cmd = self._simtel_path.joinpath("sim_telarray/bin/corsika_autoinputs")
-        cmd = str(cmd) + f" --run {corsika_bin_path}"
-        cmd += f" -R {run_number}"
-        cmd += f" -p {self._corsika_data_dir}"
-        if self._keep_seeds:
-            cmd += " --keep-seeds"
-        cmd += f" {input_tmp_file} | gzip > {log_file} 2>&1"
-        cmd += " || exit 1\n"
-        return cmd
-
-    def get_info_for_file_name(self, run_number):
-        """
-        Get a dictionary with the info necessary for building the CORSIKA runner file names.
-
-        Returns
-        -------
-        dict
-            Dictionary with the keys necessary for building the CORSIKA runner file names.
-        """
-        run_number = self._validate_run_number(run_number)
-        return {
-            "run": run_number,
-            "primary": self.corsika_config.primary,
-            "array_name": self.array_model.layout_name,
-            "site": self.array_model.site,
-            "label": self.label,
-        }
-
-    def get_file_name(self, file_type, **kwargs):
-        """
-        Get a CORSIKA style file name for various file types.
-
-        Parameters
-        ----------
-        file_type: str
-            The type of file (determines the file suffix).
-            Choices are corsika_autoinputs_log, corsika_log, script, output or sub_log.
-        kwargs: dict
-            The dictionary must include the following parameters (unless listed as optional):
-                run: int
-                    Run number.
-                primary: str
-                    Primary particle (e.g gamma, proton etc).
-                site: str
-                    Site name (usually North/South or Paranal/LaPalma).
-                array_name: str
-                    Array name.
-                label: str
-                    Instance label (optional).
-                mode: str
-                    out or err (optional, relevant only for sub_log).
-
-        Returns
-        -------
-        str
-            File name with full path.
-
-        Raises
-        ------
-        ValueError
-            If file_type is unknown.
-        """
-        file_label = (
-            f"_{kwargs['label']}" if "label" in kwargs and kwargs["label"] is not None else ""
-        )
-        file_name = (
-            f"corsika_run{kwargs['run']:06}_{kwargs['primary']}_"
-            f"{kwargs['site']}_{kwargs['array_name']}{file_label}"
-        )
-
-        if file_type == "corsika_autoinputs_log":
-            return self._corsika_log_dir.joinpath(f"log_{file_name}.log.gz")
-        if file_type == "corsika_log":
-            run_dir = self._get_run_directory(kwargs["run"])
-            return self._corsika_data_dir.joinpath(run_dir).joinpath(f"run{kwargs['run']}.log")
-        if file_type == "script":
-            script_file_dir = self._output_directory.joinpath("scripts")
-            script_file_dir.mkdir(parents=True, exist_ok=True)
-            return script_file_dir.joinpath(f"{file_name}.sh")
-        if file_type == "output":
-            zenith = self.corsika_config.get_user_parameter("THETAP")[0]
-            azimuth = self.corsika_config.get_user_parameter("AZM")[0]
-            file_name = (
-                f"corsika_run{kwargs['run']:06}_{kwargs['primary']}_"
-                f"za{round(zenith):03}deg_azm{round(azimuth):03}deg_"
-                f"{kwargs['site']}_{kwargs['array_name']}{file_label}"
-            )
-            run_dir = self._get_run_directory(kwargs["run"])
-            return self._corsika_data_dir.joinpath(run_dir).joinpath(f"{file_name}.zst")
-        if file_type == "sub_log":
-            suffix = ".log"
-            if "mode" in kwargs and kwargs["mode"] != "":
-                suffix = f".{kwargs['mode']}"
-            sub_log_file_dir = self._output_directory.joinpath("logs")
-            sub_log_file_dir.mkdir(parents=True, exist_ok=True)
-            return sub_log_file_dir.joinpath(f"log_sub_{file_name}{suffix}")
-
-        raise ValueError(f"The requested file type ({file_type}) is unknown")
-
-    def has_file(self, file_type, run_number=None, mode="out"):
-        """
-        Check that the file of file_type for the specified run number exists.
-
-        Parameters
-        ----------
-        file_type: str
-            File type to check.
-            Choices are corsika_autoinputs_log, corsika_log, script, output or sub_log.
-        run_number: int
-            Run number.
-
-        """
-        info_for_file_name = self.get_info_for_file_name(run_number)
-        run_sub_file = self.get_file_name(file_type, **info_for_file_name, mode=mode)
-        self._logger.debug(f"Checking if {run_sub_file} exists")
-        return Path(run_sub_file).is_file()
-
-    def get_resources(self, run_number=None):
-        """
-        Read run time of job from last line of submission log file.
-
-        Parameters
-        ----------
-        run_number: int
-            Run number.
-
-        Returns
-        -------
-        dict
-            run time and number of simulated events
-
-        """
-        sub_log_file = self.get_file_name(
-            file_type="sub_log", **self.get_info_for_file_name(run_number), mode="out"
-        )
-
-        self._logger.debug(f"Reading resources from {sub_log_file}")
-
-        _resources = {}
-
-        _resources["runtime"] = None
-        with open(sub_log_file, encoding="utf-8") as file:
-            for line in reversed(list(file)):
-                if "RUNTIME" in line:
-                    _resources["runtime"] = int(line.split()[1])
-                    break
-
-        if _resources["runtime"] is None:
-            self._logger.debug("RUNTIME was not found in run log file")
-
-        # Calculating number of events
-        _resources["n_events"] = int(self.corsika_config.get_user_parameter("NSHOW"))
-
-        return _resources
-
-    @staticmethod
-    def _get_run_directory(run_number):
-        """Get run directory created by sim_telarray (ex. run000014)."""
-        nn = str(run_number)
-        return "run" + nn.zfill(6)
-
-    def _validate_run_number(self, run_number):
-        """
-        Return the run number from corsika_config in case run_number is None.
-
-        Raise ValueError if run_number is not valid (< 1) or returns run_number
-        if it is a valid value.
-        """
-        if run_number is None:
-            return self.corsika_config.get_user_parameter("RUNNR")
-        if not float(run_number).is_integer() or run_number < 1:
-            msg = f"Invalid type of run number ({run_number}) - it must be an uint."
-            self._logger.error(msg)
-            raise ValueError(msg)
-
-        return run_number
diff --git a/simtools/corsika_simtel/corsika_simtel_runner.py b/simtools/corsika_simtel/corsika_simtel_runner.py
deleted file mode 100644
index 464dfaecd..000000000
--- a/simtools/corsika_simtel/corsika_simtel_runner.py
+++ /dev/null
@@ -1,196 +0,0 @@
-"""Run simulations with CORSIKA and pipe it to sim_telarray using the multipipe functionality."""
-
-import stat
-from pathlib import Path
-
-from simtools.corsika.corsika_runner import CorsikaRunner
-from simtools.simtel.simulator_array import SimulatorArray
-
-__all__ = ["CorsikaSimtelRunner"]
-
-
-class CorsikaSimtelRunner(CorsikaRunner, SimulatorArray):
-    """
-    Run simulations with CORSIKA and pipe it to sim_telarray using the multipipe functionality.
-
-    CORSIKA is set up using corsika_autoinputs program
-    provided by the sim_telarray package. It creates the multipipe script and sim_telarray command
-    corresponding to the requested configuration.
-
-    It uses CorsikaConfig to manage the CORSIKA configuration and SimulatorArray
-    for the sim_telarray configuration. User parameters must be given by the
-    common_args, corsika_args and simtel_args arguments.
-    The corsika_args and simtel_args are explained in
-    CorsikaRunner and SimulatorArray respectively.
-    An example of the common_args is given below.
-
-    .. code-block:: python
-
-        common_args = {
-            'label': 'test-production',
-            'simtel_path': '/workdir/sim_telarray/',
-        }
-
-    Parameters
-    ----------
-    common_args: dict
-        Arguments common to both CORSIKA and sim_telarray runners
-    corsika_args: dict
-        Arguments for the CORSIKA runner (see full list in CorsikaRunner documentation).
-    simtel_args: dict
-        Arguments for the sim_telarray runner (see full list in SimulatorArray documentation).
-    """
-
-    def __init__(self, common_args=None, corsika_args=None, simtel_args=None):
-        CorsikaRunner.__init__(self, use_multipipe=True, **(common_args | corsika_args))
-        SimulatorArray.__init__(self, **(common_args | simtel_args))
-
-    def prepare_run_script(self, use_pfp=False, **kwargs):
-        """
-        Get the full path of the run script file for a given run number.
-
-        Parameters
-        ----------
-        use_pfp: bool
-            Whether to use the preprocessor in preparing the CORSIKA input file
-        kwargs: dict
-            The following optional parameters can be provided:
-                run_number: int
-                    Run number.
-
-        Returns
-        -------
-        Path:
-            Full path of the run script file.
-        """
-        self.export_multipipe_script(**kwargs)
-        return CorsikaRunner.prepare_run_script(self, use_pfp=use_pfp, **kwargs)
-
-    def export_multipipe_script(self, **kwargs):
-        """
-        Write the multipipe script used in piping CORSIKA to sim_telarray.
-
-        Parameters
-        ----------
-        kwargs: dict
-            The following optional parameters can be provided:
-                run_number: int
-                    Run number.
-
-        Returns
-        -------
-        Path:
-            Full path of the run script file.
-        """
-        kwargs = {
-            "run_number": None,
-            **kwargs,
-        }
-        run_number = self._validate_run_number(kwargs["run_number"])
-
-        run_command = self._make_run_command(
-            run_number=run_number,
-            input_file="-",  # Tell sim_telarray to take the input from standard output
-        )
-        multipipe_file = Path(self.corsika_config.config_file_path.parent).joinpath(
-            self.corsika_config.get_file_name("multipipe")
-        )
-        with open(multipipe_file, "w", encoding="utf-8") as file:
-            file.write(f"{run_command}")
-        self._export_multipipe_executable(multipipe_file)
-
-    def _export_multipipe_executable(self, multipipe_file):
-        """
-        Write the multipipe executable used to call the multipipe_corsika command.
-
-        Parameters
-        ----------
-        multipipe_file: str or Path
-            The name of the multipipe file which contains all of the multipipe commands.
-        """
-        multipipe_executable = Path(self.corsika_config.config_file_path.parent).joinpath(
-            "run_cta_multipipe"
-        )
-        with open(multipipe_executable, "w", encoding="utf-8") as file:
-            multipipe_command = Path(self._simtel_path).joinpath(
-                "sim_telarray/bin/multipipe_corsika "
-                f"-c {multipipe_file}"
-                " || echo 'Fan-out failed'"
-            )
-            file.write(f"{multipipe_command}")
-
-        multipipe_executable.chmod(multipipe_executable.stat().st_mode | stat.S_IEXEC)
-
-    def _make_run_command(self, **kwargs):
-        """
-        Build and return the command to run simtel_array.
-
-        Parameters
-        ----------
-        kwargs: dict
-            The dictionary must include the following parameters (unless listed as optional):
-                input_file: str
-                    Full path of the input CORSIKA file.
-                    Use '-' to tell sim_telarray to read from standard output
-                run_number: int
-                    run number
-
-        """
-        info_for_file_name = SimulatorArray.get_info_for_file_name(self, kwargs["run_number"])
-        weak_pointing = any(pointing in self.label for pointing in ["divergent", "convergent"])
-
-        command = str(self._simtel_path.joinpath("sim_telarray/bin/sim_telarray"))
-        command += f" -c {self.array_model.get_config_file()}"
-        command += f" -I{self.array_model.get_config_directory()}"
-        command += super()._config_option(
-            "telescope_theta", self.config.zenith_angle, weak_option=weak_pointing
-        )
-        command += super()._config_option(
-            "telescope_phi", self.config.azimuth_angle, weak_option=weak_pointing
-        )
-        command += super()._config_option("power_law", abs(self.corsika_config.eslope))
-        command += super()._config_option(
-            "histogram_file", self.get_file_name("histogram", **info_for_file_name)
-        )
-        command += super()._config_option(
-            "output_file", self.get_file_name("output", **info_for_file_name)
-        )
-        command += super()._config_option("random_state", "none")
-        command += super()._config_option("show", "all")
-        command += f" {kwargs['input_file']}"
-        command += f" | gzip > {self.get_file_name('log', **info_for_file_name)} 2>&1 || exit"
-
-        return command
-
-    def get_file_name(self, file_type, run_number=None, **kwargs):
-        """
-        Get a CORSIKA or sim_telarray style file name for various file types.
-
-        See the implementations in CorsikaRunner and SimulatorArray for details.
-        """
-        if file_type in ["output", "log", "histogram"]:
-            return SimulatorArray.get_file_name(self, file_type=file_type, **kwargs)
-        return CorsikaRunner.get_file_name(
-            self, file_type=file_type, run_number=run_number, **kwargs
-        )
-
-    def get_info_for_file_name(self, run_number):
-        """
-        Get a dictionary necessary for building a CORSIKA or sim_telarray runner file names.
-
-        Returns
-        -------
-        dict
-            Dictionary with the keys necessary for building
-            a CORSIKA or sim_telarray runner file names.
-        """
-        run_number = self._validate_run_number(run_number)
-        return {
-            "run": run_number,
-            "primary": self.corsika_config.primary,
-            "array_name": self.array_model.layout_name,
-            "site": self.array_model.site,
-            "label": self.label,
-            "zenith": self.config.zenith_angle,
-            "azimuth": self.config.azimuth_angle,
-        }
diff --git a/simtools/db/db_handler.py b/simtools/db/db_handler.py
index f06bb18c4..f28feaeea 100644
--- a/simtools/db/db_handler.py
+++ b/simtools/db/db_handler.py
@@ -63,7 +63,6 @@ def __init__(self, mongo_db_config=None):
 
         """
         self._logger = logging.getLogger(__name__)
-        self._logger.debug("Initialize DatabaseHandler")
 
         self.mongo_db_config = mongo_db_config
         self.io_handler = io_handler.IOHandler()
@@ -226,12 +225,10 @@ def export_model_files(self, parameters, dest):
 
         """
         if self.mongo_db_config:
-            self._logger.debug("Exporting model files from MongoDB")
             for info in parameters.values():
                 if not info or not info.get("file") or info["value"] is None:
                     continue
                 if Path(dest).joinpath(info["value"]).exists():
-                    self._logger.debug(f"File {info['value']} already exists in {dest}")
                     continue
                 file = self._get_file_mongo_db(self._get_db_name(), info["value"])
                 self._write_file_from_mongo_to_disk(self._get_db_name(), dest, file)
@@ -339,10 +336,6 @@ def get_site_parameters(
         """
         _site, _, _model_version = self._validate_model_input(site, None, model_version)
         _db_name = self._get_db_name()
-        self._logger.debug(
-            f"Getting {site} parameters from MongoDB {_db_name}"
-            f" {model_version} {only_applicable}"
-        )
         _site_cache_key = self._parameter_cache_key(site, None, model_version)
         try:
             return DatabaseHandler.site_parameters_cached[_site_cache_key]
diff --git a/simtools/model/array_model.py b/simtools/model/array_model.py
index 20da8074c..1a6e85cef 100644
--- a/simtools/model/array_model.py
+++ b/simtools/model/array_model.py
@@ -14,11 +14,7 @@
 from simtools.simtel.simtel_config_writer import SimtelConfigWriter
 from simtools.utils import general, names
 
-__all__ = ["ArrayModel", "InvalidArrayConfigDataError"]
-
-
-class InvalidArrayConfigDataError(Exception):
-    """Exception for invalid array configuration data."""
+__all__ = ["ArrayModel"]
 
 
 class ArrayModel:
@@ -40,8 +36,6 @@ class ArrayModel:
     array_elements: str, Path, list
         Array element definitions (list of array element or path to file with
         the array element positions).
-    parameters_to_change: dict
-        Dict with the parameters to be changed with respect to the DB model.
     """
 
     def __init__(
@@ -52,7 +46,6 @@ def __init__(
         site=None,
         layout_name=None,
         array_elements=None,
-        parameters_to_change=None,
     ):
         """Initialize ArrayModel."""
         self._logger = logging.getLogger(__name__)
@@ -67,13 +60,13 @@ def __init__(
         self.db = db_handler.DatabaseHandler(mongo_db_config=mongo_db_config)
 
         self.array_elements, self.site_model, self.telescope_model = self._initialize(
-            site, array_elements, parameters_to_change
+            site, array_elements
         )
 
         self._telescope_model_files_exported = False
         self._array_model_file_exported = False
 
-    def _initialize(self, site, array_elements_config, parameters_to_change):
+    def _initialize(self, site, array_elements_config):
         """
         Initialize ArrayModel taking different configuration options into account.
 
@@ -83,8 +76,6 @@ def _initialize(self, site, array_elements_config, parameters_to_change):
             Site name.
         array_elements_config: str, Path, list
             Array element definitions.
-        parameters_to_change: dict
-            Dict with the parameters to be changed with respect to the DB model.
 
         Returns
         -------
@@ -117,11 +108,7 @@ def _initialize(self, site, array_elements_config, parameters_to_change):
             array_elements = self._get_array_elements_from_list(
                 site_model.get_array_elements_for_layout(self.layout_name)
             )
-
-        self._set_config_file_directory()
-        telescope_model = self._build_telescope_models(
-            site_model, array_elements, parameters_to_change
-        )
+        telescope_model = self._build_telescope_models(site_model, array_elements)
         return array_elements, site_model, telescope_model
 
     @property
@@ -148,11 +135,7 @@ def site(self):
         """
         return self.site_model.site
 
-    def _set_config_file_directory(self):
-        """Define and create config file directory."""
-        self._config_file_directory = self.io_handler.get_output_directory(self.label, "model")
-
-    def _build_telescope_models(self, site_model, array_elements, parameters_to_change=None):
+    def _build_telescope_models(self, site_model, array_elements):
         """
         Build the the telescope models for all telescopes of this array.
 
@@ -166,8 +149,6 @@ def _build_telescope_models(self, site_model, array_elements, parameters_to_chan
             Site model.
         array_elements: dict
             Dict with array elements.
-        parameters_to_change: dict
-            Dict with the parameters to be changed with respect to the DB model.
 
         Returns
         -------
@@ -185,77 +166,8 @@ def _build_telescope_models(self, site_model, array_elements, parameters_to_chan
                     mongo_db_config=self.mongo_db_config,
                     label=self.label,
                 )
-            # Collecting parameters to change from array_config_data
-            pars_to_change = self._get_single_telescope_info_from_array_config(
-                element_name, parameters_to_change
-            )
-            if len(pars_to_change) > 0:
-                self._logger.debug(
-                    f"Changing {len(pars_to_change)} parameters of "
-                    f"{element_name}: "
-                    f"{*pars_to_change, }, ..."
-                )
-                if element_name in telescope_model:
-                    telescope_model[element_name].change_multiple_parameters(**pars_to_change)
-                    telescope_model[element_name].set_extra_label(element_name)
-
         return telescope_model
 
-    def _get_single_telescope_info_from_array_config(self, tel_name, array_config_data):
-        """
-        Return telescope information from array configuration data.
-
-        array_config_data contains the name of default telescope models for each telescope type
-        and the list of specific telescopes. For each case, the data can be given only as a name \
-        or as a dict with 'name' and parameters to change. This function has to identify these \
-        two cases and collect the telescope name and the dict with the parameters to change.
-
-        Parameters
-        ----------
-        tel_name: str
-            Name of the telescope at the layout level (LSTN-01, MSTN-05, ...).
-        array_config_data: dict
-            Dict with the array config data.
-        """
-
-        def _process_single_telescope(data):
-            """
-            Read array configuration data for a single telescope.
-
-            Parameters
-            ----------
-            data: dict or str
-                Piece of the array_config_data for one specific telescope.
-
-            Returns
-            -------
-            dict
-                Dict with the parameters to change.
-            """
-            if isinstance(data, dict):
-                # Case 0: data is dict
-                if "name" not in data.keys():
-                    msg = "ArrayConfig has no name for a telescope"
-                    self._logger.error(msg)
-                    raise InvalidArrayConfigDataError(msg)
-                pars_to_change = {k: v for (k, v) in data.items() if k != "name"}
-                self._logger.debug(
-                    "Grabbing tel data as dict - " f"{len(pars_to_change)} pars to change"
-                )
-                return pars_to_change
-            if isinstance(data, str):
-                # Case 1: data is string (only name)
-                return {}
-
-            # Case 2: data has a wrong type
-            msg = "ArrayConfig has wrong input for a telescope"
-            self._logger.error(msg)
-            raise InvalidArrayConfigDataError(msg)
-
-        if array_config_data and tel_name in array_config_data.keys():
-            return _process_single_telescope(array_config_data[tel_name])
-        return {}
-
     def print_telescope_list(self):
         """Print list of telescopes."""
         for tel_name, data in self.telescope_model.items():
@@ -288,7 +200,7 @@ def export_simtel_array_config_file(self):
             model_version=self.model_version,
             label=self.label,
         )
-        self._config_file_path = self._config_file_directory.joinpath(config_file_name)
+        self._config_file_path = self.get_config_directory().joinpath(config_file_name)
 
         # Writing parameters to the file
         self._logger.info(f"Writing array configuration file into {self._config_file_path}")
@@ -339,6 +251,8 @@ def get_config_directory(self):
         Path
             Path of the config directory path for sim_telarray.
         """
+        if self._config_file_directory is None:
+            self._config_file_directory = self.io_handler.get_output_directory(self.label, "model")
         return self._config_file_directory
 
     def _load_array_element_positions_from_file(self, array_elements_file, site):
diff --git a/simtools/model/model_parameter.py b/simtools/model/model_parameter.py
index 6e6e7390d..83f8abf4f 100644
--- a/simtools/model/model_parameter.py
+++ b/simtools/model/model_parameter.py
@@ -294,10 +294,6 @@ def _load_parameters_from_db(self):
             return
 
         if self.name is not None:
-            self._logger.debug(
-                f"Reading array element parameters from DB "
-                f"({self.name}, {self.model_version}, {self.site}, {self.collection})"
-            )
             self._parameters = self.db.get_model_parameters(
                 self.site, self.name, self.model_version, self.collection, only_applicable=True
             )
@@ -314,7 +310,6 @@ def _load_parameters_from_db(self):
                 )
 
         if self.site is not None:
-            self._logger.debug(f"Reading site parameters from DB ({self.site} site)")
             _site_pars = self.db.get_site_parameters(
                 self.site, self.model_version, only_applicable=True
             )
diff --git a/simtools/ray_tracing.py b/simtools/ray_tracing.py
index e6ea6d6ef..0ead30f88 100644
--- a/simtools/ray_tracing.py
+++ b/simtools/ray_tracing.py
@@ -193,7 +193,7 @@ def simulate(self, test=False, force=False):
                     },
                     force_simulate=force,
                 )
-                simtel.run(test=test, force=force)
+                simtel.run(test=test)
 
                 photons_file_name = names.generate_file_name(
                     file_type="photons",
diff --git a/simtools/corsika_simtel/__init__.py b/simtools/runners/__init__.py
similarity index 100%
rename from simtools/corsika_simtel/__init__.py
rename to simtools/runners/__init__.py
diff --git a/simtools/runners/corsika_runner.py b/simtools/runners/corsika_runner.py
new file mode 100644
index 000000000..0d225a68c
--- /dev/null
+++ b/simtools/runners/corsika_runner.py
@@ -0,0 +1,232 @@
+"""Generate run scripts and directories for CORSIKA simulations."""
+
+import logging
+import os
+from pathlib import Path
+
+from simtools.io_operations import io_handler
+from simtools.runners.runner_services import RunnerServices
+
+__all__ = ["CorsikaRunner", "MissingRequiredEntryInCorsikaConfigError"]
+
+
+class MissingRequiredEntryInCorsikaConfigError(Exception):
+    """Exception for missing required entry in corsika config."""
+
+
+class CorsikaRunner:
+    """
+    Generate run scripts and directories for CORSIKA simulations. Run simulations if requested.
+
+    CorsikaRunner is responsible for configuring and running CORSIKA, using corsika_autoinputs
+    provided by the sim_telarray package. CorsikaRunner generates shell scripts to be run
+    externally or by the simulator module simulator.
+
+    CorsikaRunner is configured through a CorsikaConfig instance.
+
+    Parameters
+    ----------
+    corsika_config_data: CorsikaConfig
+        CORSIKA configuration.
+    simtel_path: str or Path
+        Location of source of the sim_telarray/CORSIKA package.
+    label: str
+        Instance label.
+    keep_seeds: bool
+        Use seeds based on run number and primary particle. If False, use sim_telarray seeds.
+    use_multipipe: bool
+        Use multipipe to run CORSIKA and sim_telarray.
+    """
+
+    def __init__(
+        self,
+        corsika_config,
+        simtel_path,
+        label=None,
+        keep_seeds=False,
+        use_multipipe=False,
+    ):
+        """Initialize CorsikaRunner."""
+        self._logger = logging.getLogger(__name__)
+        self._logger.debug("Init CorsikaRunner")
+        self.label = label
+
+        self.corsika_config = corsika_config
+        self._keep_seeds = keep_seeds
+        self._use_multipipe = use_multipipe
+
+        self._simtel_path = Path(simtel_path)
+        self.io_handler = io_handler.IOHandler()
+
+        self.runner_service = RunnerServices(corsika_config, label)
+        self._directory = self.runner_service.load_data_directories("corsika")
+
+    def prepare_run_script(
+        self, run_number=None, extra_commands=None, input_file=None, use_pfp=True
+    ):
+        """
+        Get the full path of the run script file for a given run number.
+
+        Parameters
+        ----------
+        use_pfp: bool
+            Whether to use the preprocessor in preparing the CORSIKA input file
+        run_number: int
+            Run number.
+        extra_commands: str
+            Additional commands for running simulations.
+
+        Returns
+        -------
+        Path:
+            Full path of the run script file.
+        """
+        if input_file is not None:
+            self._logger.warning(
+                "input_file parameter is not used in CorsikaRunner.prepare_run_script"
+            )
+        self.corsika_config.run_number = run_number
+
+        script_file_path = self.get_file_name(
+            file_type="sub_script", run_number=self.corsika_config.run_number
+        )
+        corsika_input_file = self.corsika_config.generate_corsika_input_file(self._use_multipipe)
+
+        # CORSIKA input file for a specific run, created by the preprocessor pfp
+        corsika_input_tmp_name = self.corsika_config.get_corsika_config_file_name(
+            file_type="config_tmp", run_number=self.corsika_config.run_number
+        )
+        corsika_input_tmp_file = self._directory["inputs"].joinpath(corsika_input_tmp_name)
+
+        if use_pfp:
+            pfp_command = self._get_pfp_command(corsika_input_tmp_file, corsika_input_file)
+        autoinputs_command = self._get_autoinputs_command(
+            self.corsika_config.run_number, corsika_input_tmp_file
+        )
+
+        self._logger.debug(f"Extra commands to be added to the run script: {extra_commands}")
+        self._logger.debug(f"CORSIKA data will be set to {self._directory['data']}")
+
+        with open(script_file_path, "w", encoding="utf-8") as file:
+            file.write("#!/usr/bin/env bash\n")
+            file.write("set -e\n")
+            file.write("set -o pipefail\n")
+
+            # Setting SECONDS variable to measure runtime
+            file.write("\nSECONDS=0\n")
+
+            if extra_commands is not None:
+                file.write("\n# Writing extras\n")
+                file.write(f"{extra_commands}\n")
+                file.write("# End of extras\n\n")
+
+            file.write(f"export CORSIKA_DATA={self._directory['data']}\n")
+            file.write('mkdir -p "$CORSIKA_DATA"\n')
+            file.write('cd "$CORSIKA_DATA" || exit 2\n')
+            if use_pfp:
+                file.write("\n# Running pfp\n")
+                file.write(pfp_command)
+                file.write("\n# Replacing the XXXXXX placeholder with the run number\n")
+                file.write(
+                    f"sed -i 's/XXXXXX/{self.corsika_config.run_number:06}/g' "
+                    f"{corsika_input_tmp_file}\n"
+                )
+            else:
+                file.write("\n# Copying CORSIKA input file to run location\n")
+                file.write(f"cp {corsika_input_file} {corsika_input_tmp_file}")
+            file.write("\n# Running corsika_autoinputs\n")
+            file.write(autoinputs_command)
+
+            file.write('\necho "RUNTIME: $SECONDS"\n')
+
+        os.system(f"chmod ug+x {script_file_path}")
+
+        return script_file_path
+
+    def get_resources(self, run_number=None):
+        """Return computing resources used."""
+        return self.runner_service.get_resources(run_number)
+
+    def _get_pfp_command(self, input_tmp_file, corsika_input_file):
+        """
+        Get pfp pre-processor command.
+
+        pfp is a pre-processor tool and part of sim_telarray.
+
+        Parameters
+        ----------
+        input_tmp_file: Path
+            Temporary input file.
+
+        Returns
+        -------
+        str
+            pfp command.
+        """
+        cmd = self._simtel_path.joinpath("sim_telarray/bin/pfp")
+        cmd = str(cmd) + f" -V -DWITHOUT_MULTIPIPE - < {corsika_input_file}"
+        cmd += f" > {input_tmp_file} || exit\n"
+        return cmd
+
+    def _get_autoinputs_command(self, run_number, input_tmp_file):
+        """
+        Get autoinputs command.
+
+        corsika_autoinputs is a tool to generate random and user/host dependent
+        parameters for CORSIKA configuration.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+        input_tmp_file: Path
+            Temporary input file.
+
+        Returns
+        -------
+        str
+            autoinputs command.
+        """
+        corsika_bin_path = self._simtel_path.joinpath("corsika-run/corsika")
+
+        log_file = self.get_file_name(file_type="log", run_number=run_number)
+
+        cmd = self._simtel_path.joinpath("sim_telarray/bin/corsika_autoinputs")
+        cmd = str(cmd) + f" --run {corsika_bin_path}"
+        cmd += f" -R {run_number}"
+        cmd += ' -p "$CORSIKA_DATA"'
+        if self._keep_seeds:
+            cmd += " --keep-seeds"
+        cmd += f" {input_tmp_file} | gzip > {log_file} 2>&1"
+        cmd += " || exit 1\n"
+        return cmd
+
+    def get_file_name(
+        self, simulation_software="corsika", file_type=None, run_number=None, mode=""
+    ):
+        """
+        Get the full path of a file for a given run number.
+
+        Parameters
+        ----------
+        simulation_software: str
+            Simulation software.
+        file_type: str
+            File type.
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        str
+            File name with full path.
+        """
+        if simulation_software.lower() != "corsika":
+            raise ValueError(
+                f"simulation_software ({simulation_software}) is not supported in CorsikaRunner"
+            )
+        return self.runner_service.get_file_name(
+            file_type=file_type,
+            run_number=run_number,
+            mode=mode,
+        )
diff --git a/simtools/runners/corsika_simtel_runner.py b/simtools/runners/corsika_simtel_runner.py
new file mode 100644
index 000000000..4427fa0db
--- /dev/null
+++ b/simtools/runners/corsika_simtel_runner.py
@@ -0,0 +1,215 @@
+"""Run simulations with CORSIKA and pipe it to sim_telarray using the multipipe functionality."""
+
+import logging
+import stat
+from pathlib import Path
+
+from simtools.runners.corsika_runner import CorsikaRunner
+from simtools.simtel.simulator_array import SimulatorArray
+
+__all__ = ["CorsikaSimtelRunner"]
+
+
+class CorsikaSimtelRunner:
+    """
+    Run simulations with CORSIKA and pipe it to sim_telarray using the multipipe functionality.
+
+    Uses CorsikaConfig to manage the CORSIKA configuration and SimulatorArray
+    for the sim_telarray configuration.
+
+    Parameters
+    ----------
+    corsika_config : CorsikaConfig
+        Configuration parameters for CORSIKA.
+    simtel_path : str or Path
+        Location of the sim_telarray package.
+    label : str
+        Label.
+    keep_seeds : bool
+        Use seeds based on run number and primary particle. If False, use sim_telarray seeds.
+    use_multipipe : bool
+        Use multipipe to run CORSIKA and sim_telarray.
+    """
+
+    def __init__(
+        self,
+        corsika_config,
+        simtel_path,
+        label=None,
+        keep_seeds=False,
+        use_multipipe=False,
+    ):
+        self._logger = logging.getLogger(__name__)
+        self.corsika_config = corsika_config
+        self._simtel_path = simtel_path
+        self.label = label
+
+        self.corsika_config.set_output_file_and_directory(use_multipipe)
+        self.corsika_runner = CorsikaRunner(
+            corsika_config=corsika_config,
+            simtel_path=simtel_path,
+            label=label,
+            keep_seeds=keep_seeds,
+            use_multipipe=use_multipipe,
+        )
+        self.simulator_array = SimulatorArray(
+            corsika_config=corsika_config,
+            simtel_path=simtel_path,
+            label=label,
+            use_multipipe=use_multipipe,
+        )
+
+    def prepare_run_script(
+        self, run_number=None, input_file=None, extra_commands=None, use_pfp=False
+    ):
+        """
+        Get the full path of the run script file for a given run number.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+        use_pfp: bool
+            Whether to use the preprocessor in preparing the CORSIKA input file
+
+        Returns
+        -------
+        Path:
+            Full path of the run script file.
+        """
+        self._export_multipipe_script(run_number)
+        return self.corsika_runner.prepare_run_script(
+            run_number=run_number,
+            input_file=input_file,
+            extra_commands=extra_commands,
+            use_pfp=use_pfp,
+        )
+
+    def _export_multipipe_script(self, run_number):
+        """
+        Write the multipipe script used in piping CORSIKA to sim_telarray.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        Path:
+            Full path of the run script file.
+        """
+        run_command = self._make_run_command(
+            run_number=run_number,
+            input_file="-",  # instruct sim_telarray to take input from standard output
+        )
+        multipipe_file = Path(self.corsika_config.config_file_path.parent).joinpath(
+            self.corsika_config.get_corsika_config_file_name("multipipe")
+        )
+        with open(multipipe_file, "w", encoding="utf-8") as file:
+            file.write(f"{run_command}")
+        self._logger.info(f"Multipipe script: {multipipe_file}")
+        self._write_multipipe_script(multipipe_file)
+
+    def _write_multipipe_script(self, multipipe_file):
+        """
+        Write script used to call the multipipe_corsika command.
+
+        Parameters
+        ----------
+        multipipe_file: str or Path
+            The name of the multipipe file which contains all of the multipipe commands.
+        """
+        multipipe_script = Path(self.corsika_config.config_file_path.parent).joinpath(
+            "run_cta_multipipe"
+        )
+        with open(multipipe_script, "w", encoding="utf-8") as file:
+            multipipe_command = Path(self._simtel_path).joinpath(
+                "sim_telarray/bin/multipipe_corsika "
+                f"-c {multipipe_file}"
+                " || echo 'Fan-out failed'"
+            )
+            file.write(f"{multipipe_command}")
+
+        multipipe_script.chmod(multipipe_script.stat().st_mode | stat.S_IEXEC)
+
+    def _make_run_command(self, run_number=None, input_file=None):
+        """
+        Build and return the command to run simtel_array.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+        input_file: str
+            Full path of the input CORSIKA file.
+            Use '-' to tell sim_telarray to read from standard output
+
+        Returns
+        -------
+        str:
+            Command to run sim_telarray.
+        """
+        try:
+            weak_pointing = any(pointing in self.label for pointing in ["divergent", "convergent"])
+        except TypeError:  # allow for self.label to be None
+            weak_pointing = False
+
+        command = str(self._simtel_path.joinpath("sim_telarray/bin/sim_telarray"))
+        command += f" -c {self.corsika_config.array_model.get_config_file()}"
+        command += f" -I{self.corsika_config.array_model.get_config_directory()}"
+        command += self.simulator_array.get_config_option(
+            "telescope_theta", self.corsika_config.zenith_angle, weak_option=weak_pointing
+        )
+        command += self.simulator_array.get_config_option(
+            "telescope_phi", self.corsika_config.azimuth_angle, weak_option=weak_pointing
+        )
+        command += self.simulator_array.get_config_option(
+            "power_law", abs(self.corsika_config.get_config_parameter("ESLOPE"))
+        )
+        command += self.simulator_array.get_config_option(
+            "histogram_file",
+            self.get_file_name(
+                simulation_software="simtel", file_type="histogram", run_number=run_number
+            ),
+        )
+        command += self.simulator_array.get_config_option(
+            "output_file",
+            self.simulator_array.get_file_name(
+                simulation_software="simtel", file_type="output", run_number=run_number
+            ),
+        )
+        command += self.simulator_array.get_config_option("random_state", "none")
+        command += self.simulator_array.get_config_option("show", "all")
+        command += f" {input_file}"
+        _log_file = self.simulator_array.get_file_name(
+            simulation_software="simtel", file_type="log", run_number=run_number
+        )
+        command += f" | gzip > {_log_file} 2>&1 || exit"
+
+        return command
+
+    def get_file_name(self, simulation_software=None, file_type=None, run_number=None, mode=None):
+        """
+        Get the full path of a file for a given run number.
+
+        Parameters
+        ----------
+        simulation_software: str
+            Simulation software.
+        file_type: str
+            File type.
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        str
+            File name with full path.
+        """
+        if simulation_software is None:
+            # preference to simtel output (multipipe)
+            simulation_software = "simtel" if self.simulator_array else "corsika"
+
+        runner = self.corsika_runner if simulation_software == "corsika" else self.simulator_array
+        return runner.get_file_name(file_type=file_type, run_number=run_number, mode=mode)
diff --git a/simtools/runners/runner_services.py b/simtools/runners/runner_services.py
new file mode 100644
index 000000000..216b71619
--- /dev/null
+++ b/simtools/runners/runner_services.py
@@ -0,0 +1,304 @@
+"""Base service methods for simulation runners."""
+
+import logging
+from pathlib import Path
+
+from simtools.io_operations import io_handler
+
+_logger = logging.getLogger(__name__)
+
+
+class RunnerServices:
+    """
+    Base services for simulation runners.
+
+    Parameters
+    ----------
+    corsika_config : CorsikaConfig
+        Configuration parameters for CORSIKA.
+    label : str
+        Label.
+    """
+
+    def __init__(self, corsika_config, label=None):
+        """Initialize RunnerServices."""
+        self._logger = logging.getLogger(__name__)
+        self._logger.debug("Init RunnerServices")
+        self.label = label
+        self.corsika_config = corsika_config
+        self.directory = {}
+
+    def _get_info_for_file_name(self, run_number):
+        """
+        Return dictionary for building names for simulation output files.
+
+        Parameters
+        ----------
+        run_number : int
+            Run number.
+
+        Returns
+        -------
+        dict
+            Dictionary with the keys or building the file names for simulation output files.
+        """
+        return {
+            "run_number": self.corsika_config.validate_run_number(run_number),
+            "primary": self.corsika_config.primary,
+            "array_name": self.corsika_config.array_model.layout_name,
+            "site": self.corsika_config.array_model.site,
+            "label": self.label,
+            "zenith": self.corsika_config.zenith_angle,
+            "azimuth": self.corsika_config.azimuth_angle,
+        }
+
+    @staticmethod
+    def _get_simulation_software_list(simulation_software):
+        """
+        Return a list of simulation software based on the input string.
+
+        Args:
+            simulation_software: String representing the desired software.
+
+        Returns
+        -------
+            List of simulation software names.
+        """
+        software_map = {
+            "corsika": ["corsika"],
+            "simtel": ["simtel"],
+            "corsika_simtel": ["corsika", "simtel"],
+        }
+        return software_map.get(simulation_software.lower(), [])
+
+    def load_data_directories(self, simulation_software):
+        """
+        Create and return directories for output, data, log and input.
+
+        Parameters
+        ----------
+        simulation_software : str
+            Simulation software to be used.
+
+        Returns
+        -------
+        dict
+            Dictionary containing paths requires for simulation configuration.
+        """
+        self.directory["output"] = io_handler.IOHandler().get_output_directory(self.label)
+        _logger.debug(f"Creating output dir {self.directory['output']}")
+        for dir_name in ["sub_scripts", "sub_logs"]:
+            self.directory[dir_name] = self.directory["output"].joinpath(dir_name)
+            self.directory[dir_name].mkdir(parents=True, exist_ok=True)
+        for _simulation_software in self._get_simulation_software_list(simulation_software):
+            for dir_name in ["data", "inputs", "logs"]:
+                self.directory[dir_name] = self.directory["output"].joinpath(
+                    _simulation_software, dir_name
+                )
+                self.directory[dir_name].mkdir(parents=True, exist_ok=True)
+        self._logger.debug(f"Data directories for {simulation_software}: {self.directory}")
+        return self.directory
+
+    def has_file(self, file_type, run_number=None, mode="out"):
+        """
+        Check that the file of file_type for the specified run number exists.
+
+        Parameters
+        ----------
+        file_type: str
+            File type to check.
+        run_number: int
+            Run number.
+
+        """
+        run_sub_file = self.get_file_name(file_type, run_number=run_number, mode=mode)
+        _logger.debug(f"Checking if {run_sub_file} exists")
+        return Path(run_sub_file).is_file()
+
+    def _get_file_basename(self, run_number):
+        """
+        Get the base name for the simulation files.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        str
+            Base name for the simulation files.
+        """
+        info_for_file_name = self._get_info_for_file_name(run_number)
+        file_label = f"{info_for_file_name.get('label', '')}"
+        file_label = f"_{file_label}" if file_label else ""
+        zenith = self.corsika_config.get_config_parameter("THETAP")[0]
+        azimuth = self.corsika_config.azimuth_angle
+        run_dir = self._get_run_number_string(info_for_file_name["run_number"])
+        return (
+            f"{run_dir}_{info_for_file_name['primary']}_"
+            f"za{round(zenith):02}deg_azm{azimuth:03}deg_"
+            f"{info_for_file_name['site']}_{info_for_file_name['array_name']}{file_label}"
+        )
+
+    def _get_log_file_path(self, file_type, file_name):
+        """
+        Return path for log files.
+
+        Parameters
+        ----------
+        file_type : str
+            File type.
+        file_name : str
+            File name.
+
+        Returns
+        -------
+        Path
+            Path for log files.
+        """
+        log_suffixes = {
+            "log": ".log.gz",
+            "histogram": ".hdata.zst",
+        }
+        return self.directory["logs"].joinpath(f"{file_name}{log_suffixes[file_type]}")
+
+    def _get_data_file_path(self, file_type, file_name, run_number):
+        """
+        Return path for data files.
+
+        Parameters
+        ----------
+        file_type : str
+            File type.
+        file_name : str
+            File name.
+        run_number : int
+            Run number.
+
+        Returns
+        -------
+        Path
+            Path for data files.
+        """
+        data_suffixes = {
+            "output": ".zst",
+            "corsika_output": ".zst",
+            "corsika_log": ".log",
+            "simtel_output": ".simtel.zst",
+        }
+        run_dir = self._get_run_number_string(run_number)
+        data_run_dir = self.directory["data"].joinpath(run_dir)
+        data_run_dir.mkdir(parents=True, exist_ok=True)
+        return data_run_dir.joinpath(f"{file_name}{data_suffixes[file_type]}")
+
+    def _get_sub_file_path(self, file_type, file_name, mode):
+        """
+        Return path for submission files.
+
+        Parameters
+        ----------
+        file_type : str
+            File type.
+        file_name : str
+            File name.
+        mode : str
+            Mode (out or err).
+
+        Returns
+        -------
+        Path
+            Path for submission files.
+        """
+        suffix = ".log" if file_type == "sub_log" else ".sh"
+        if mode and mode != "":
+            suffix = f".{mode}"
+        sub_log_file_dir = self.directory["output"].joinpath(f"{file_type}s")
+        sub_log_file_dir.mkdir(parents=True, exist_ok=True)
+        return sub_log_file_dir.joinpath(f"sub_{file_name}{suffix}")
+
+    def get_file_name(self, file_type, run_number=None, mode=None):
+        """
+        Get a CORSIKA/sim_telarray style file name for various log and data file types.
+
+        Parameters
+        ----------
+        file_type : str
+            The type of file (determines the file suffix).
+        run_number : int
+            Run number.
+        mode: str
+            out or err (optional, relevant only for sub_log).
+
+        Returns
+        -------
+        str
+            File name with full path.
+
+        Raises
+        ------
+        ValueError
+            If file_type is unknown.
+        """
+        file_name = self._get_file_basename(run_number)
+
+        if file_type in ["log", "histogram"]:
+            return self._get_log_file_path(file_type, file_name)
+
+        if file_type in ["output", "corsika_output", "corsika_log", "simtel_output"]:
+            return self._get_data_file_path(file_type, file_name, run_number)
+
+        if file_type in ("sub_log", "sub_script"):
+            return self._get_sub_file_path(file_type, file_name, mode)
+
+        raise ValueError(f"The requested file type ({file_type}) is unknown")
+
+    @staticmethod
+    def _get_run_number_string(run_number):
+        """
+        Get run number string as used for the simulation file names(ex. run000014).
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        str
+            Run number string.
+        """
+        nn = str(run_number)
+        if len(nn) > 6:
+            raise ValueError("Run number cannot have more than 6 digits")
+        return "run" + nn.zfill(6)
+
+    def get_resources(self, run_number=None):
+        """
+        Read run time of job from last line of submission log file.
+
+        Parameters
+        ----------
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        dict
+            run time and number of simulated events
+        """
+        sub_log_file = self.get_file_name(file_type="sub_log", run_number=run_number, mode="out")
+        _logger.debug(f"Reading resources from {sub_log_file}")
+
+        _resources = {}
+        _resources["runtime"] = None
+        with open(sub_log_file, encoding="utf-8") as file:
+            for line in reversed(list(file)):
+                if "RUNTIME" in line:
+                    _resources["runtime"] = int(line.split()[1])
+                    break
+
+        if _resources["runtime"] is None:
+            _logger.debug("RUNTIME was not found in run log file")
+        _resources["n_events"] = int(self.corsika_config.get_config_parameter("NSHOW"))
+        return _resources
diff --git a/simtools/simtel/simtel_runner.py b/simtools/runners/simtel_runner.py
similarity index 64%
rename from simtools/simtel/simtel_runner.py
rename to simtools/runners/simtel_runner.py
index c956dfcb7..4de9655af 100644
--- a/simtools/simtel/simtel_runner.py
+++ b/simtools/runners/simtel_runner.py
@@ -5,13 +5,10 @@
 from pathlib import Path
 
 import simtools.utils.general as gen
+from simtools.runners.runner_services import RunnerServices
 
 __all__ = ["InvalidOutputFileError", "SimtelExecutionError", "SimtelRunner"]
 
-# pylint: disable=no-member
-# The line above is needed because there are methods which are used in this class
-# but are implemented in the classes inheriting from it.
-
 
 class SimtelExecutionError(Exception):
     """Exception for simtel_array execution error."""
@@ -23,7 +20,7 @@ class InvalidOutputFileError(Exception):
 
 class SimtelRunner:
     """
-    SimtelRunner is the base class of the sim_telarray interfaces.
+    Base class for running sim_telarray simulations.
 
     Parameters
     ----------
@@ -33,17 +30,21 @@ class SimtelRunner:
         Instance label. Important for output file naming.
     """
 
-    def __init__(self, simtel_path, label=None):
+    def __init__(self, simtel_path, label=None, corsika_config=None, use_multipipe=False):
         """Initialize SimtelRunner."""
         self._logger = logging.getLogger(__name__)
 
         self._simtel_path = Path(simtel_path)
         self.label = label
-        self._script_dir = None
-        self._script_file = None
+        self._base_directory = None
 
         self.runs_per_set = 1
 
+        self.runner_service = RunnerServices(corsika_config, label)
+        self._directory = self.runner_service.load_data_directories(
+            "corsika_simtel" if use_multipipe else "simtel"
+        )
+
     def __repr__(self):
         """Return a string representation of the SimtelRunner object."""
         return f"SimtelRunner(label={self.label})\n"
@@ -70,24 +71,17 @@ def prepare_run_script(self, test=False, input_file=None, run_number=None, extra
         """
         self._logger.debug("Creating run bash script")
 
-        self._script_dir = self._base_directory.joinpath("scripts")
-        self._script_dir.mkdir(parents=True, exist_ok=True)
-        self._script_file = self._script_dir.joinpath(
-            f"run{run_number if run_number is not None else ''}-simtel"
-        )
-        self._logger.debug(f"Run bash script - {self._script_file}")
+        script_file_path = self.get_file_name(file_type="sub_script", run_number=run_number)
+
+        self._logger.debug(f"Run bash script - {script_file_path}")
 
         self._logger.debug(f"Extra commands to be added to the run script {extra_commands}")
 
-        command = self._make_run_command(input_file=input_file, run_number=run_number)
-        with self._script_file.open("w", encoding="utf-8") as file:
+        command = self._make_run_command(run_number=run_number, input_file=input_file)
+        with script_file_path.open("w", encoding="utf-8") as file:
             file.write("#!/usr/bin/env bash\n\n")
-
-            # Make sure to exit on failed commands and report their error code
             file.write("set -e\n")
             file.write("set -o pipefail\n")
-
-            # Setting SECONDS variable to measure runtime
             file.write("\nSECONDS=0\n")
 
             if extra_commands is not None:
@@ -100,13 +94,12 @@ def prepare_run_script(self, test=False, input_file=None, run_number=None, extra
             for _ in range(n):
                 file.write(f"{command}\n\n")
 
-            # Printing out runtime
             file.write('\necho "RUNTIME: $SECONDS"\n')
 
-        os.system(f"chmod ug+x {self._script_file}")
-        return self._script_file
+        os.system(f"chmod ug+x {script_file_path}")
+        return script_file_path
 
-    def run(self, test=False, force=False, input_file=None, run_number=None):
+    def run(self, test=False, input_file=None, run_number=None):
         """
         Make run command and run sim_telarray.
 
@@ -114,8 +107,6 @@ def run(self, test=False, force=False, input_file=None, run_number=None):
         ----------
         test: bool
             If True, make simulations faster.
-        force: bool
-            If True, remove possible existing output files and run again.
         input_file: str or Path
             Full path of the input CORSIKA file.
         run_number: int
@@ -123,39 +114,21 @@ def run(self, test=False, force=False, input_file=None, run_number=None):
         """
         self._logger.debug("Running sim_telarray")
 
-        if not hasattr(self, "_make_run_command"):
-            msg = "run method cannot be executed without the _make_run_command method"
-            self._logger.error(msg)
-            raise RuntimeError(msg)
-
-        if not self._shall_run() and not force:
-            self._logger.info("Skipping because output exists and force = False")
-            return
-
-        command = self._make_run_command(input_file=input_file, run_number=run_number)
+        command = self._make_run_command(run_number=run_number, input_file=input_file)
 
         if test:
             self._logger.info(f"Running (test) with command: {command}")
             self._run_simtel_and_check_output(command)
         else:
             self._logger.debug(f"Running ({self.runs_per_set}x) with command: {command}")
-            self._run_simtel_and_check_output(command)
-
-            for _ in range(self.runs_per_set - 1):
+            for _ in range(self.runs_per_set):
                 self._run_simtel_and_check_output(command)
 
         self._check_run_result(run_number=run_number)
 
-    @staticmethod
-    def _simtel_failed(sys_output):
-        """Test if simtel process ended successfully.
-
-        Returns
-        -------
-        bool
-            1 if sys_output is different than 0, and 1 otherwise.
-        """
-        return sys_output != 0
+    def _check_run_result(self, run_number=None):  # pylint: disable=all
+        """Check if simtel output file exists."""
+        pass
 
     def _raise_simtel_error(self):
         """
@@ -184,19 +157,20 @@ def _run_simtel_and_check_output(self, command):
         SimtelExecutionError
             if run was not successful.
         """
-        sys_output = os.system(command)
-        if self._simtel_failed(sys_output):
+        if os.system(command) != 0:
             self._raise_simtel_error()
 
-    def _shall_run(self):
+    def _make_run_command(self, run_number=None, input_file=None):
         self._logger.debug(
-            "shall_run is being called from the base class - returning False -"
+            "make_run_command is being called from the base class - "
             "it should be implemented in the sub class"
         )
-        return False
+        input_file = input_file if input_file else "nofile"
+        run_number = run_number if run_number else 1
+        return f"{input_file}-{run_number}"
 
     @staticmethod
-    def _config_option(par, value=None, weak_option=False):
+    def get_config_option(par, value=None, weak_option=False):
         """
         Build sim_telarray command.
 
@@ -218,3 +192,33 @@ def _config_option(par, value=None, weak_option=False):
         c = f" {option_syntax} {par}"
         c += f"={value}" if value is not None else ""
         return c
+
+    def get_resources(self, run_number=None):
+        """Return computing resources used."""
+        return self.runner_service.get_resources(run_number)
+
+    def get_file_name(self, simulation_software="simtel", file_type=None, run_number=None, mode=""):
+        """
+        Get the full path of a file for a given run number.
+
+        Parameters
+        ----------
+        simulation_software: str
+            Simulation software.
+        file_type: str
+            File type.
+        run_number: int
+            Run number.
+
+        Returns
+        -------
+        str
+            File name with full path.
+        """
+        if simulation_software.lower() != "simtel":
+            raise ValueError(
+                f"simulation_software ({simulation_software}) is not supported in SimulatorArray"
+            )
+        return self.runner_service.get_file_name(
+            file_type=file_type, run_number=run_number, mode=mode
+        )
diff --git a/simtools/simtel/simulator_array.py b/simtools/simtel/simulator_array.py
index 1c91ad560..b4617414a 100644
--- a/simtools/simtel/simulator_array.py
+++ b/simtools/simtel/simulator_array.py
@@ -1,11 +1,9 @@
 """Simulation runner for array simulations."""
 
 import logging
-from pathlib import Path
 
-import simtools.utils.general as gen
 from simtools.io_operations import io_handler
-from simtools.simtel.simtel_runner import InvalidOutputFileError, SimtelRunner
+from simtools.runners.simtel_runner import InvalidOutputFileError, SimtelRunner
 
 __all__ = ["SimulatorArray"]
 
@@ -14,195 +12,82 @@ class SimulatorArray(SimtelRunner):
     """
     SimulatorArray is the interface with sim_telarray to perform array simulations.
 
-    Configurable parameters:
-        simtel_data_directory:
-            len: 1
-            default: null
-            unit: null
-        primary:
-            len: 1
-            unit: null
-        zenith_angle:
-            len: 1
-            unit: deg
-            default: 20 deg
-        azimuth_angle:
-            len: 1
-            unit: deg
-            default: 0 deg
-
     Parameters
     ----------
-    array_model: str
-        Instance of ArrayModel class.
-    label: str
-        Instance label. Important for output file naming.
+    corsika_config_data: CorsikaConfig
+        CORSIKA configuration.
     simtel_path: str or Path
-        Location of sim_telarray installation.
-    config_data: dict
-        Dict containing the configurable parameters.
-    config_file: str or Path
-        Path of the yaml file containing the configurable parameters.
+        Location of source of the sim_telarray/CORSIKA package.
+    label: str
+        Instance label.
+    keep_seeds: bool
+        Use seeds based on run number and primary particle. If False, use sim_telarray seeds.
+    use_multipipe: bool
+        Use multipipe to run CORSIKA and sim_telarray.
     """
 
     def __init__(
         self,
-        array_model,
+        corsika_config,
+        simtel_path,
         label=None,
-        simtel_path=None,
-        config_data=None,
-        config_file=None,
+        keep_seeds=False,
+        use_multipipe=False,
     ):
         """Initialize SimulatorArray."""
         self._logger = logging.getLogger(__name__)
         self._logger.debug("Init SimulatorArray")
-
-        super().__init__(label=label, simtel_path=simtel_path)
-
-        self.array_model = array_model
-        self.label = label if label is not None else self.array_model.label
-        self._log_file = None
-
-        self.io_handler = io_handler.IOHandler()
-
-        self._base_directory = self.io_handler.get_output_directory(self.label, "array-simulator")
-
-        # Loading config_data
-        _config_data_in = gen.collect_data_from_file_or_dict(config_file, config_data)
-        _parameter_file = self.io_handler.get_input_data_file(
-            "parameters", "simtel-runner-array_parameters.yml"
+        super().__init__(
+            label=label,
+            simtel_path=simtel_path,
+            corsika_config=corsika_config,
+            use_multipipe=use_multipipe,
         )
-        _parameters = gen.collect_data_from_file_or_dict(_parameter_file, None)
-        self.config = gen.validate_config_data(_config_data_in, _parameters)
-
-        self._load_simtel_data_directories()
-
-    def _load_simtel_data_directories(self):
-        """
-        Create sim_telarray output directories for data, log and input.
-
-        If simtel_data_directory is not given as a configurable parameter,
-        the standard directory of simtools output (simtools-output) will
-        be used. A sub directory simtel-data will be created and sub directories for
-        log and data will be created inside it.
-        """
-        if self.config.simtel_data_directory is None:
-            # Default config value
-            simtel_base_dir = self._base_directory
-        else:
-            simtel_base_dir = Path(self.config.simtel_data_directory)
-
-        simtel_base_dir = simtel_base_dir.joinpath("simtel-data")
-        simtel_base_dir = simtel_base_dir.joinpath(self.array_model.site)
-        simtel_base_dir = simtel_base_dir.joinpath(self.config.primary)
-        simtel_base_dir = simtel_base_dir.absolute()
-
-        self._simtel_data_dir = simtel_base_dir.joinpath("data")
-        self._simtel_data_dir.mkdir(parents=True, exist_ok=True)
-        self._simtel_log_dir = simtel_base_dir.joinpath("log")
-        self._simtel_log_dir.mkdir(parents=True, exist_ok=True)
 
-    def get_info_for_file_name(self, run_number):
-        """
-        Get a dictionary with the info necessary for building the sim_telarray file names.
-
-        Returns
-        -------
-        dict
-            Dictionary with the keys necessary for building the sim_telarray file names.
-        """
-        return {
-            "run": run_number,
-            "primary": self.config.primary,
-            "array_name": self.array_model.layout_name,
-            "site": self.array_model.site,
-            "zenith": self.config.zenith_angle,
-            "azimuth": self.config.azimuth_angle,
-            "label": self.label,
-        }
+        self.corsika_config = corsika_config
+        self.io_handler = io_handler.IOHandler()
+        self._log_file = None
+        self.keep_seeds = keep_seeds
 
-    def get_file_name(self, file_type, **kwargs):
+    def _make_run_command(self, run_number=None, input_file=None):
         """
-        Get a sim_telarray style file name for various file types.
+        Build and return the command to run simtel_array.
 
         Parameters
         ----------
-        file_type: str
-            The type of file (determines the file suffix). Choices are log, histogram, output or\
-             sub_log.
-        kwargs: dict
-            The dictionary must include the following parameters (unless listed as optional):
-                run: int
-                    Run number.
-                primary: str
-                    Primary particle (e.g gamma, proton etc).
-                zenith: float
-                    Zenith angle (deg).
-                azimuth: float
-                    Azimuth angle (deg).
-                site: str
-                    Site name (usually North/South or Paranal/LaPalma).
-                array_name: str
-                    Array name.
-                label: str
-                    Instance label (optional).
-                mode: str
-                    out or err (optional, relevant only for sub_log).
+        input_file: str
+            Full path of the input CORSIKA file
+        run_number: int (optional)
+            run number
 
         Returns
         -------
         str
-            File name with full path.
-
-        Raises
-        ------
-        ValueError
-            If file_type is unknown.
+            Command to run sim_telarray.
         """
-        file_label = (
-            f"_{kwargs['label']}" if "label" in kwargs and kwargs["label"] is not None else ""
-        )
-        file_name = (
-            f"run{kwargs['run']:06}_{kwargs['primary']}_"
-            f"za{round(kwargs['zenith']):03}deg_azm{round(kwargs['azimuth']):03}deg_"
-            f"{kwargs['site']}_{kwargs['array_name']}{file_label}"
-        )
-        if file_type == "log":
-            return self._simtel_log_dir.joinpath(f"{file_name}.log.gz")
-        if file_type == "histogram":
-            return self._simtel_log_dir.joinpath(f"{file_name}.hdata.zst")
-        if file_type == "output":
-            return self._simtel_data_dir.joinpath(f"{file_name}.simtel.zst")
-        if file_type == "sub_log":
-            suffix = ".log"
-            if "mode" in kwargs:
-                suffix = f".{kwargs['mode']}"
-            return self._simtel_log_dir.joinpath(f"log_sub_{file_name}{suffix}")
-
-        raise ValueError(f"The requested file type ({file_type}) is unknown")
+        self._log_file = self.get_file_name(file_type="log", run_number=run_number)
+        histogram_file = self.get_file_name(file_type="histogram", run_number=run_number)
+        output_file = self.get_file_name(file_type="output", run_number=run_number)
 
-    def has_file(self, file_type, run_number, mode="out"):
-        """
-        Check that the file of file_type for the specified run number exists.
+        # Array
+        command = str(self._simtel_path.joinpath("sim_telarray/bin/sim_telarray"))
+        command += f" -c {self.corsika_config.array_model.get_config_file()}"
+        command += f" -I{self.corsika_config.array_model.get_config_directory()}"
+        command += super().get_config_option("telescope_theta", self.corsika_config.zenith_angle)
+        command += super().get_config_option("telescope_phi", self.corsika_config.azimuth_angle)
+        command += super().get_config_option("power_law", "2.5")
+        command += super().get_config_option("histogram_file", histogram_file)
+        command += super().get_config_option("output_file", output_file)
+        command += super().get_config_option("random_state", "none")
+        command += super().get_config_option("show", "all")
+        command += f" {input_file}"
+        command += f" > {self._log_file} 2>&1 || exit"
 
-        Parameters
-        ----------
-        file_type: str
-            File type to check.
-            Choices are log, histogram, output or sub_log.
-        run_number: int
-            Run number.
-        mode: str
-            Mode.
-        """
-        info_for_file_name = self.get_info_for_file_name(run_number)
-        run_sub_file = self.get_file_name(file_type, **info_for_file_name, mode=mode)
-        self._logger.debug(f"Checking if {run_sub_file} exists")
-        return Path(run_sub_file).is_file()
+        return command
 
-    def get_resources(self, run_number):
+    def _check_run_result(self, run_number=None):
         """
-        Read run time from last line of submission log file.
+        Check if simtel output file exists.
 
         Parameters
         ----------
@@ -211,78 +96,18 @@ def get_resources(self, run_number):
 
         Returns
         -------
-        dict
-            run time of job in seconds.
-
-        """
-        info_for_file_name = self.get_info_for_file_name(run_number)
-        sub_log_file = self.get_file_name("sub_log", **info_for_file_name, mode="out")
-
-        self._logger.debug(f"Reading resources from {sub_log_file}")
-
-        _resources = {}
-
-        _resources["runtime"] = None
-        with open(sub_log_file, encoding="utf-8") as file:
-            for line in reversed(list(file)):
-                if "RUNTIME" in line:
-                    _resources["runtime"] = int(line.split()[1])
-                    break
-
-        if _resources["runtime"] is None:
-            self._logger.debug("RUNTIME was not found in run log file")
-
-        return _resources
-
-    def _shall_run(self, **kwargs):
-        """Tells if simulations should be run again based on the existence of output files."""
-        output_file = self.get_file_name(
-            file_type="output", **self.get_info_for_file_name(kwargs["run_number"])
-        )
-        return not output_file.exists()
-
-    def _make_run_command(self, **kwargs):
-        """
-        Build and return the command to run simtel_array.
+        bool
+            True if simtel output file exists.
 
-        Parameters
-        ----------
-        kwargs: dict
-            The dictionary must include the following parameters (unless listed as optional):
-                input_file: str
-                    Full path of the input CORSIKA file
-                run_number: int (optional)
-                    run number
+        Raises
+        ------
+        InvalidOutputFileError
+            If simtel output file does not exist.
         """
-        run_number = kwargs["run_number"] if "run_number" in kwargs else 1
-        info_for_file_name = self.get_info_for_file_name(run_number)
-        self._log_file = self.get_file_name(file_type="log", **info_for_file_name)
-        histogram_file = self.get_file_name(file_type="histogram", **info_for_file_name)
-        output_file = self.get_file_name(file_type="output", **info_for_file_name)
-
-        # Array
-        command = str(self._simtel_path.joinpath("sim_telarray/bin/sim_telarray"))
-        command += f" -c {self.array_model.get_config_file()}"
-        command += f" -I{self.array_model.get_config_directory()}"
-        command += super()._config_option("telescope_theta", self.config.zenith_angle)
-        command += super()._config_option("telescope_phi", self.config.azimuth_angle)
-        command += super()._config_option("power_law", "2.5")
-        command += super()._config_option("histogram_file", histogram_file)
-        command += super()._config_option("output_file", output_file)
-        command += super()._config_option("random_state", "none")
-        command += super()._config_option("show", "all")
-        command += f" {kwargs['input_file']}"
-        command += f" > {self._log_file} 2>&1 || exit"
-
-        return command
-
-    def _check_run_result(self, **kwargs):
-        """Check run results."""
-        output_file = self.get_file_name(
-            file_type="output", **self.get_info_for_file_name(kwargs["run_number"])
-        )
+        output_file = self.get_file_name(file_type="output", run_number=run_number)
         if not output_file.exists():
-            msg = "sim_telarray output file does not exist."
+            msg = f"sim_telarray output file {output_file} does not exist."
             self._logger.error(msg)
             raise InvalidOutputFileError(msg)
         self._logger.debug(f"simtel_array output file {output_file} exists.")
+        return True
diff --git a/simtools/simtel/simulator_camera_efficiency.py b/simtools/simtel/simulator_camera_efficiency.py
index a9c01a49d..4b8011e0c 100644
--- a/simtools/simtel/simulator_camera_efficiency.py
+++ b/simtools/simtel/simulator_camera_efficiency.py
@@ -4,7 +4,7 @@
 from pathlib import Path
 
 from simtools.model.model_parameter import InvalidModelParameterError
-from simtools.simtel.simtel_runner import SimtelRunner
+from simtools.runners.simtel_runner import SimtelRunner
 
 __all__ = ["SimulatorCameraEfficiency"]
 
@@ -66,11 +66,9 @@ def nsb_spectrum(self, nsb_spectrum):
         else:
             self._nsb_spectrum = None
 
-    def _shall_run(self, **kwargs):  # pylint: disable=unused-argument; applies only to this line
-        """Tells if simulations should be run again based on the existence of output files."""
-        return not self._file_simtel.exists()
-
-    def _make_run_command(self, **kwargs):  # pylint: disable=unused-argument
+    def _make_run_command(
+        self, run_number=None, input_file=None
+    ):  # pylint: disable=unused-argument
         """Prepare the command used to run testeff."""
         self._logger.debug("Preparing the command to run testeff")
 
@@ -148,10 +146,9 @@ def _make_run_command(self, **kwargs):  # pylint: disable=unused-argument
         command += f" 2>{self._file_log}"
         command += f" >{self._file_simtel}"
 
-        # Moving to sim_telarray directory before running
         return f"cd {self._simtel_path.joinpath('sim_telarray')} && {command}"
 
-    def _check_run_result(self, **kwargs):  # pylint: disable=unused-argument
+    def _check_run_result(self, run_number=None):  # pylint: disable=unused-argument
         """Check run results.
 
         Raises
diff --git a/simtools/simtel/simulator_light_emission.py b/simtools/simtel/simulator_light_emission.py
index e639a9b05..d1db289b8 100644
--- a/simtools/simtel/simulator_light_emission.py
+++ b/simtools/simtel/simulator_light_emission.py
@@ -8,7 +8,7 @@
 
 import simtools.utils.general as gen
 from simtools.io_operations import io_handler
-from simtools.simtel.simtel_runner import SimtelRunner
+from simtools.runners.simtel_runner import SimtelRunner
 
 __all__ = ["SimulatorLightEmission"]
 
@@ -394,32 +394,31 @@ def remove_line_from_config(file_path, line_prefix):
         command += " -DNUM_TELESCOPES=1"
         command += " -I../cfg/CTA"
         command += "iobuf_maximum=1000000000"
-        command += super()._config_option(
+        command += super().get_config_option(
             "altitude", self._site_model.get_parameter_value("corsika_observation_level")
         )
-        command += super()._config_option(
+        command += super().get_config_option(
             "atmospheric_transmission",
             self._telescope_model.get_parameter_value("atmospheric_transmission"),
         )
-        # command += super()._config_option("show", "all") # for debugging
-        command += super()._config_option("TRIGGER_CURRENT_LIMIT", "20")
-        command += super()._config_option("TRIGGER_TELESCOPES", "1")
-        command += super()._config_option("TELTRIG_MIN_SIGSUM", "7.8")
-        command += super()._config_option("PULSE_ANALYSIS", "-30")
+        command += super().get_config_option("TRIGGER_CURRENT_LIMIT", "20")
+        command += super().get_config_option("TRIGGER_TELESCOPES", "1")
+        command += super().get_config_option("TELTRIG_MIN_SIGSUM", "7.8")
+        command += super().get_config_option("PULSE_ANALYSIS", "-30")
 
         if "real" in self.default_le_config["x_pos"]:
             _, angles = self.calibration_pointing_direction()
-            command += super()._config_option("telescope_theta", f"{angles[0]}")
-            command += super()._config_option("telescope_phi", f"{angles[1]}")
+            command += super().get_config_option("telescope_theta", f"{angles[0]}")
+            command += super().get_config_option("telescope_phi", f"{angles[1]}")
         else:
-            command += super()._config_option("telescope_theta", 0)
-            command += super()._config_option("telescope_phi", 0)
+            command += super().get_config_option("telescope_theta", 0)
+            command += super().get_config_option("telescope_phi", 0)
 
-        command += super()._config_option("power_law", "2.68")
-        command += super()._config_option(
+        command += super().get_config_option("power_law", "2.68")
+        command += super().get_config_option(
             "input_file", f"{self.output_directory}/{self.le_application[0]}.iact.gz"
         )
-        command += super()._config_option(
+        command += super().get_config_option(
             "output_file",
             f"{self.output_directory}/"
             f"{self.le_application[0]}_{self.le_application[1]}.simtel.gz\n",
@@ -493,15 +492,15 @@ def prepare_script(self, generate_postscript=False, **kwargs):
         """
         self._logger.debug("Creating run bash script")
 
-        self._script_dir = self.output_directory.joinpath("scripts")
-        self._script_dir.mkdir(parents=True, exist_ok=True)
-        self._script_file = self._script_dir.joinpath(f"{self.le_application[0]}-lightemission.sh")
-        self._logger.debug(f"Run bash script - {self._script_file}")
+        _script_dir = self.output_directory.joinpath("scripts")
+        _script_dir.mkdir(parents=True, exist_ok=True)
+        _script_file = _script_dir.joinpath(f"{self.le_application[0]}-lightemission.sh")
+        self._logger.debug(f"Run bash script - {_script_file}")
 
         command_le = self._make_light_emission_script()
         command_simtel = self._make_simtel_script()
 
-        with self._script_file.open("w", encoding="utf-8") as file:
+        with _script_file.open("w", encoding="utf-8") as file:
             file.write("#!/usr/bin/env bash\n\n")
 
             file.write(f"{command_le}\n\n")
@@ -514,5 +513,5 @@ def prepare_script(self, generate_postscript=False, **kwargs):
                 file.write(f"{command_plot}\n\n")
                 file.write("# End\n\n")
 
-        os.system(f"chmod ug+x {self._script_file}")
-        return self._script_file
+        os.system(f"chmod ug+x {_script_file}")
+        return _script_file
diff --git a/simtools/simtel/simulator_ray_tracing.py b/simtools/simtel/simulator_ray_tracing.py
index 6afb8e457..c5099b3b9 100644
--- a/simtools/simtel/simulator_ray_tracing.py
+++ b/simtools/simtel/simulator_ray_tracing.py
@@ -6,7 +6,7 @@
 
 import simtools.utils.general as gen
 from simtools.io_operations import io_handler
-from simtools.simtel.simtel_runner import SimtelRunner
+from simtools.runners.simtel_runner import SimtelRunner
 from simtools.utils import names
 
 __all__ = ["SimulatorRayTracing"]
@@ -156,11 +156,9 @@ def _load_required_files(self, force_simulate):
                     f"0. {90.0 - self.config.zenith_angle} 1.0 {self.config.source_distance}\n"
                 )
 
-    def _shall_run(self, **kwargs):  # pylint: disable=unused-argument; applies only to this line
-        """Tells if simulations should be run again based on the existence of output files."""
-        return not self._is_photon_list_file_ok()
-
-    def _make_run_command(self, **kwargs):  # pylint: disable=unused-argument
+    def _make_run_command(
+        self, run_number=None, input_file=None
+    ):  # pylint: disable=unused-argument
         """Return the command to run simtel_array."""
         if self.config.single_mirror_mode:
             _mirror_focal_length = float(
@@ -172,52 +170,52 @@ def _make_run_command(self, **kwargs):  # pylint: disable=unused-argument
         command += f" -c {self.telescope_model.get_config_file()}"
         command += " -I../cfg/CTA"
         command += f" -I{self.telescope_model.config_file_directory}"
-        command += super()._config_option("random_state", "none")
-        command += super()._config_option("IMAGING_LIST", str(self._photons_file))
-        command += super()._config_option("stars", str(self._stars_file))
-        command += super()._config_option(
+        command += super().get_config_option("random_state", "none")
+        command += super().get_config_option("IMAGING_LIST", str(self._photons_file))
+        command += super().get_config_option("stars", str(self._stars_file))
+        command += super().get_config_option(
             "altitude", self.telescope_model.get_parameter_value("corsika_observation_level")
         )
-        command += super()._config_option(
+        command += super().get_config_option(
             "telescope_theta", self.config.zenith_angle + self.config.off_axis_angle
         )
-        command += super()._config_option("star_photons", str(self.photons_per_run))
-        command += super()._config_option("telescope_phi", "0")
-        command += super()._config_option("camera_transmission", "1.0")
-        command += super()._config_option("nightsky_background", "all:0.")
-        command += super()._config_option("trigger_current_limit", "1e10")
-        command += super()._config_option("telescope_random_angle", "0")
-        command += super()._config_option("telescope_random_error", "0")
-        command += super()._config_option("convergent_depth", "0")
-        command += super()._config_option("maximum_telescopes", "1")
-        command += super()._config_option("show", "all")
-        command += super()._config_option("camera_filter", "none")
+        command += super().get_config_option("star_photons", str(self.photons_per_run))
+        command += super().get_config_option("telescope_phi", "0")
+        command += super().get_config_option("camera_transmission", "1.0")
+        command += super().get_config_option("nightsky_background", "all:0.")
+        command += super().get_config_option("trigger_current_limit", "1e10")
+        command += super().get_config_option("telescope_random_angle", "0")
+        command += super().get_config_option("telescope_random_error", "0")
+        command += super().get_config_option("convergent_depth", "0")
+        command += super().get_config_option("maximum_telescopes", "1")
+        command += super().get_config_option("show", "all")
+        command += super().get_config_option("camera_filter", "none")
         if self.config.single_mirror_mode:
-            command += super()._config_option("focus_offset", "all:0.")
-            command += super()._config_option("camera_config_file", "single_pixel_camera.dat")
-            command += super()._config_option("camera_pixels", "1")
-            command += super()._config_option("trigger_pixels", "1")
-            command += super()._config_option("camera_body_diameter", "0")
-            command += super()._config_option(
+            command += super().get_config_option("focus_offset", "all:0.")
+            command += super().get_config_option("camera_config_file", "single_pixel_camera.dat")
+            command += super().get_config_option("camera_pixels", "1")
+            command += super().get_config_option("trigger_pixels", "1")
+            command += super().get_config_option("camera_body_diameter", "0")
+            command += super().get_config_option(
                 "mirror_list",
                 self.telescope_model.get_single_mirror_list_file(
                     self.config.mirror_numbers, self.config.use_random_focal_length
                 ),
             )
-            command += super()._config_option(
+            command += super().get_config_option(
                 "focal_length", self.config.source_distance * u.km.to(u.cm)
             )
-            command += super()._config_option("dish_shape_length", _mirror_focal_length)
-            command += super()._config_option("mirror_focal_length", _mirror_focal_length)
-            command += super()._config_option("parabolic_dish", "0")
-            command += super()._config_option("mirror_align_random_distance", "0.")
-            command += super()._config_option("mirror_align_random_vertical", "0.,28.,0.,0.")
+            command += super().get_config_option("dish_shape_length", _mirror_focal_length)
+            command += super().get_config_option("mirror_focal_length", _mirror_focal_length)
+            command += super().get_config_option("parabolic_dish", "0")
+            command += super().get_config_option("mirror_align_random_distance", "0.")
+            command += super().get_config_option("mirror_align_random_vertical", "0.,28.,0.,0.")
         command += " " + str(self._corsika_file)
         command += " 2>&1 > " + str(self._log_file) + " 2>&1"
 
         return command
 
-    def _check_run_result(self, **kwargs):  # pylint: disable=unused-argument
+    def _check_run_result(self, run_number=None):  # pylint: disable=unused-argument
         """Check run results.
 
         Raises
diff --git a/simtools/simulator.py b/simtools/simulator.py
index fb9aec507..d46491f3c 100644
--- a/simtools/simulator.py
+++ b/simtools/simulator.py
@@ -3,20 +3,17 @@
 import logging
 import re
 from collections import defaultdict
-from copy import copy
 from pathlib import Path
 
-import astropy.units as u
 import numpy as np
 
 import simtools.utils.general as gen
-from simtools.corsika.corsika_runner import CorsikaRunner
-from simtools.corsika_simtel.corsika_simtel_runner import CorsikaSimtelRunner
-from simtools.io_operations import io_handler
+from simtools.corsika.corsika_config import CorsikaConfig
 from simtools.job_execution.job_manager import JobManager
 from simtools.model.array_model import ArrayModel
+from simtools.runners.corsika_runner import CorsikaRunner
+from simtools.runners.corsika_simtel_runner import CorsikaSimtelRunner
 from simtools.simtel.simulator_array import SimulatorArray
-from simtools.utils import names
 
 __all__ = [
     "Simulator",
@@ -30,115 +27,55 @@ class InvalidRunsToSimulateError(Exception):
 
 class Simulator:
     """
-    Simulator is responsible for managing simulation of showers and array of telescopes.
-
-    It interfaces with simulation software-specific packages, like CORSIKA or sim_telarray.
-
-    The configuration is set as a dict config_data or a yaml \
-    file config_file.
-
-    Example of config_data for shower simulations:
-
-    .. code-block:: python
-
-        config_data = {
-            'data_directory': '.',
-            'site': 'South',
-            'layout_name': 'Prod5',
-            'run_range': [1, 100],
-            'nshow': 10,
-            'primary': 'gamma',
-            'erange': [100 * u.GeV, 1 * u.TeV],
-            'eslope': -2,
-            'zenith': 20 * u.deg,
-            'azimuth': 0 * u.deg,
-            'viewcone': 0 * u.deg,
-            'cscat': [10, 1500 * u.m, 0]
-        }
-
-    Example of config_data for array simulations:
-
-    .. code-block:: python
-
-        config_data = {
-            'data_directory': '(..)/data',
-            'primary': 'gamma',
-            'zenith': 20 * u.deg,
-            'azimuth': 0 * u.deg,
-            'viewcone': 0 * u.deg,
-            # ArrayModel
-            'site': 'North',
-            'layout_name': '1LST',
-            'model_version': 'Prod5',
-            'default': {
-                'LST': '1'
-            },
-            'MST-01': 'FlashCam-D'
-        }
+    Simulator is managing the simulation of showers and of the array of telescopes.
+
+    It interfaces with simulation software packages (e.g., CORSIKA or sim_telarray).
+
+    The configuration is set as a dict corresponding to the command line configuration groups
+    (especially simulation_software, simulation_model, simulation_parameters).
 
     Parameters
     ----------
-    simulation_software: str
-        Simulation software to be used (choices: [corsika, simtel, corsika_simtel])
-    simulator_source_path: str or Path
-        Location of executables for simulation software \
-            (e.g. path with CORSIKA or sim_telarray)
+    args_dict : dict
+        Configuration dictionary
+        (includes simulation_software, simulation_model, simulation_parameters groups).
     label: str
         Instance label.
-    config_data: dict
-        Simulator configuration data.
     submit_command: str
         Job submission command.
     extra_commands: str or list of str
-        Extra commands to be added to the run script before the run command,
+        Extra commands to be added to the run script before the run command.
     mongo_db_config: dict
         MongoDB configuration.
-    model_version: str
-        Simulation model version.
     test: bool
-        If True, no jobs are submitted; only run scripts are prepared
+        If True, no jobs are submitted; only run scripts are prepared.
     """
 
     def __init__(
         self,
-        simulation_software,
-        simulator_source_path,
-        config_data,
+        args_dict,
         label=None,
         submit_command=None,
         extra_commands=None,
         mongo_db_config=None,
-        model_version=None,
         test=False,
     ):
         """Initialize Simulator class."""
         self._logger = logging.getLogger(__name__)
-        self._logger.debug(f"Init Simulator {simulation_software}")
+        self.args_dict = args_dict
 
+        self.simulation_software = self.args_dict["simulation_software"]
+        self._logger.debug(f"Init Simulator {self.simulation_software}")
         self.label = label
-        self.simulation_software = simulation_software
-        self.runs = []
-        self._results = defaultdict(list)
-        self.test = test
-
-        self._corsika_config_data = None
-        self._corsika_parameters_file = None
-        self.config = None
-        self.array_model = None
-        self._simulation_runner = None
 
-        self.io_handler = io_handler.IOHandler()
-        self._output_directory = self.io_handler.get_output_directory(
-            self.label, self.simulation_software
-        )
-        self._simulator_source_path = Path(simulator_source_path)
+        self.runs = self._initialize_run_list()
+        self._results = defaultdict(list)
+        self._test = test
         self._submit_command = submit_command
         self._extra_commands = extra_commands
-        self._mongo_db_config = mongo_db_config
-        self._model_version = model_version
 
-        self._load_configuration_and_simulation_model(config_data)
-        self._set_simulation_runner()
+        self.array_model = self._initialize_array_model(mongo_db_config)
+        self._simulation_runner = self._initialize_simulation_runner()
 
     @property
     def simulation_software(self):
@@ -162,77 +99,59 @@ def simulation_software(self, simulation_software):
 
         """
         if simulation_software not in ["simtel", "corsika", "corsika_simtel"]:
+            self._logger.error(f"Invalid simulation software: {simulation_software}")
             raise gen.InvalidConfigDataError
         self._simulation_software = simulation_software.lower()
 
-    def _load_configuration_and_simulation_model(self, config_data):
+    def _initialize_array_model(self, mongo_db_config):
         """
-        Load configuration data and initialize simulation models.
+        Initialize array simulation model.
 
         Parameters
         ----------
-        config_data: dict
-            Simulator configuration data.
+        mongo_db_config: dict
+            Database configuration.
 
+        Returns
+        -------
+        ArrayModel
+            ArrayModel object.
         """
-        self._load_corsika_config_and_model(config_data)
-        self._load_sim_tel_config_and_model(config_data)
-
-        self.array_model = ArrayModel(
+        return ArrayModel(
             label=self.label,
-            site=config_data["common"]["site"],
-            layout_name=config_data["common"]["layout_name"],
-            parameters_to_change=config_data,
-            mongo_db_config=self._mongo_db_config,
-            model_version=self._model_version,
+            site=self.args_dict.get("site"),
+            layout_name=self.args_dict.get("array_layout_name"),
+            mongo_db_config=mongo_db_config,
+            model_version=self.args_dict.get("model_version", None),
         )
 
-    def _load_corsika_config_and_model(self, config_data):
+    def _initialize_run_list(self):
         """
-        Load configuration data for CORSIKA shower simulation.
-
-        Validate configuration data for CORSIKA shower simulation and
-        remove entries not needed for CorsikaRunner.
-
-        Parameters
-        ----------
-        config_data: dict
-            Simulator configuration data.
+        Initialize run list using the configuration values 'run_number_start' and 'number_of_runs'.
 
-        """
-        self._corsika_config_data = copy(config_data["common"])
-        self._corsika_config_data.update(copy(config_data["showers"]))
-        self.runs = self._validate_run_list_and_range(
-            self._corsika_config_data.pop("run_list", None),
-            self._corsika_config_data.pop("run_range", None),
-        )
-        for key in ("site", "layout_name"):
-            try:
-                self._corsika_config_data.pop(key)
-            except KeyError:
-                pass
-
-        self._corsika_parameters_file = self._corsika_config_data.pop(
-            "corsika_parameters_file", None
-        )
-
-    def _load_sim_tel_config_and_model(self, config_data):
-        """
-        Load array model and configuration parameters for array simulations.
-
-        Parameters
-        ----------
-        config_data: dict
-            Simulator configuration data.
+        Returns
+        -------
+        list
+            List of run numbers.
 
+        Raises
+        ------
+        KeyError
+            If 'run_number_start' or 'number_of_runs' are not found in the configuration.
         """
-        _rest_config = self._collect_array_model_parameters(config_data)
-
-        _parameter_file = self.io_handler.get_input_data_file(
-            "parameters", "array-simulator_parameters.yml"
-        )
-        _parameters = gen.collect_data_from_file_or_dict(_parameter_file, None)
-        self.config = gen.validate_config_data(_rest_config, _parameters, ignore_unidentified=True)
+        try:
+            return self._validate_run_list_and_range(
+                run_list=None,
+                run_range=[
+                    self.args_dict["run_number_start"],
+                    self.args_dict["run_number_start"] + self.args_dict["number_of_runs"],
+                ],
+            )
+        except KeyError as exc:
+            self._logger.error(
+                "Error in initializing run list (missing 'run_number_start' or 'number_of_runs')"
+            )
+            raise exc
 
     def _validate_run_list_and_range(self, run_list, run_range):
         """
@@ -265,8 +184,6 @@ def _validate_run_list_and_range(self, run_list, run_range):
                 msg = "run_list must contain only integers."
                 self._logger.error(msg)
                 raise InvalidRunsToSimulateError
-
-            self._logger.debug(f"run_list: {run_list}")
             validated_runs = list(run_list)
 
         if run_range is not None:
@@ -275,88 +192,51 @@ def _validate_run_list_and_range(self, run_list, run_range):
                 self._logger.error(msg)
                 raise InvalidRunsToSimulateError
 
-            run_range = np.arange(run_range[0], run_range[1] + 1)
+            run_range = np.arange(run_range[0], run_range[1])
             self._logger.debug(f"run_range: {run_range}")
             validated_runs.extend(list(run_range))
 
         validated_runs_unique = sorted(set(validated_runs))
+        self._logger.info(f"run_list: {validated_runs_unique}")
         return list(validated_runs_unique)
 
-    def _collect_array_model_parameters(self, config_data):
+    def _initialize_simulation_runner(self):
         """
-        Separate configuration and model parameters from configuration data.
-
-        Parameters
-        ----------
-        config_data: dict
-
-
-        """
-        _merged_config = copy(config_data["common"])
-        if "array" in config_data:
-            _merged_config.update(copy(config_data["array"]))
-        _rest_data = copy(_merged_config)
-
-        # Reading telescope keys
-        tel_keys = []
-        for key in _rest_data.keys():
-            try:
-                names.validate_telescope_name(key)
-                tel_keys.append(key)
-            except ValueError:
-                pass
-        return _rest_data
-
-    def _set_simulation_runner(self):
-        """Set simulation runners."""
-        common_args = {
-            "label": self.label,
-            "simtel_path": self._simulator_source_path,
-            "array_model": self.array_model,
-        }
-        corsika_args = {
-            "corsika_parameters_file": self._corsika_parameters_file,
-            "corsika_config_data": self._corsika_config_data,
-        }
-        simtel_args = {}
-        if self.simulation_software in ["simtel", "corsika_simtel"]:
-            simtel_args = {
-                "config_data": {
-                    "simtel_data_directory": self.config.data_directory,
-                    "primary": self.config.primary,
-                    "zenith_angle": self.config.zenith_angle * u.deg,
-                    "azimuth_angle": self.config.azimuth_angle * u.deg,
-                },
-            }
-
-        if self.simulation_software == "corsika":
-            self._set_corsika_runner(common_args | corsika_args)
-        if self.simulation_software == "simtel":
-            self._set_simtel_runner(common_args | simtel_args)
-        if self.simulation_software == "corsika_simtel":
-            self._set_corsika_simtel_runner(common_args, corsika_args, simtel_args)
+        Initialize corsika configuration and simulation runners.
 
-    def _set_corsika_runner(self, simulator_args):
-        """Create CorsikaRunner."""
-        self._simulation_runner = CorsikaRunner(**simulator_args)
+        Returns
+        -------
+        CorsikaRunner or SimulatorArray or CorsikaSimtelRunner
+            Simulation runner object.
+        """
+        corsika_config = CorsikaConfig(
+            array_model=self.array_model,
+            label=self.label,
+            args_dict=self.args_dict,
+        )
 
-    def _set_simtel_runner(self, simulator_args):
-        """Create a SimulatorArray."""
-        self._simulation_runner = SimulatorArray(**simulator_args)
+        runner_class = {
+            "corsika": CorsikaRunner,
+            "simtel": SimulatorArray,
+            "corsika_simtel": CorsikaSimtelRunner,
+        }.get(self.simulation_software)
 
-    def _set_corsika_simtel_runner(self, common_args, corsika_args, simtel_args):
-        """Create CorsikaRunner."""
-        self._simulation_runner = CorsikaSimtelRunner(common_args, corsika_args, simtel_args)
+        return runner_class(
+            label=self.label,
+            corsika_config=corsika_config,
+            simtel_path=self.args_dict.get("simtel_path"),
+            keep_seeds=False,
+            use_multipipe=runner_class is CorsikaSimtelRunner,
+        )
 
     def _fill_results_without_run(self, input_file_list):
         """
-        Fill in the results dict without calling submit.
+        Fill results dict without calling submit (e.g., for testing).
 
         Parameters
         ----------
         input_file_list: str or list of str
             Single file or list of files of shower simulations.
-
         """
         input_file_list = self._enforce_list_type(input_file_list)
 
@@ -374,7 +254,6 @@ def simulate(self, input_file_list=None):
         ----------
         input_file_list: str or list of str
             Single file or list of files of shower simulations.
-
         """
         self._logger.info(f"Submission command: {self._submit_command}")
 
@@ -386,46 +265,23 @@ def simulate(self, input_file_list=None):
             f"run{'s' if len(runs_and_files_to_submit) > 1 else ''}"
         )
 
-        for run, file in runs_and_files_to_submit.items():
+        for run_number, input_file in runs_and_files_to_submit.items():
             run_script = self._simulation_runner.prepare_run_script(
-                run_number=run, input_file=file, extra_commands=self._extra_commands
+                run_number=run_number, input_file=input_file, extra_commands=self._extra_commands
             )
 
-            job_manager = JobManager(submit_command=self._submit_command, test=self.test)
+            job_manager = JobManager(submit_command=self._submit_command, test=self._test)
             job_manager.submit(
                 run_script=run_script,
                 run_out_file=self._simulation_runner.get_file_name(
-                    file_type="sub_log", **self._simulation_runner.get_info_for_file_name(run)
+                    file_type="sub_log", run_number=run_number
                 ),
                 log_file=self._simulation_runner.get_file_name(
-                    file_type=(
-                        "corsika_autoinputs_log" if self.simulation_software == "corsika" else "log"
-                    ),
-                    **self._simulation_runner.get_info_for_file_name(run),
+                    file_type=("log"), run_number=run_number
                 ),
             )
 
-            self._fill_results(file, run)
-
-    def file_list(self, input_file_list=None):
-        """
-        List output files obtained with simulation run.
-
-        Parameters
-        ----------
-        input_file_list: str or list of str
-            Single file or list of files of shower simulations.
-
-        """
-        runs_and_files_to_submit = self._get_runs_and_files_to_submit(
-            input_file_list=input_file_list
-        )
-
-        for run, _ in runs_and_files_to_submit.items():
-            output_file_name = self._simulation_runner.get_file_name(
-                file_type="output", **self._simulation_runner.get_info_for_file_name(run)
-            )
-            print(f"{output_file_name!s} (file exists: {Path.exists(output_file_name)})")
+            self._fill_results(input_file, run_number)
 
     def _get_runs_and_files_to_submit(self, input_file_list=None):
         """
@@ -444,29 +300,42 @@ def _get_runs_and_files_to_submit(self, input_file_list=None):
             dictionary with run number as key and (if available) simulation
             file name as value
 
+        Raises
+        ------
+        ValueError
+            If no runs are to be submitted.
+
         """
         _runs_and_files = {}
+        self._logger.debug(f"Getting runs and files to submit ({input_file_list})")
 
         if self.simulation_software == "simtel":
-            _file_list = self._enforce_list_type(input_file_list)
-            for file in _file_list:
-                _runs_and_files[self._guess_run_from_file(file)] = file
-        if self.simulation_software in ["corsika", "corsika_simtel"]:
-            _run_list = self._get_runs_to_simulate()
-            for run in _run_list:
-                _runs_and_files[run] = None
-
+            input_file_list = self._enforce_list_type(input_file_list)
+            _runs_and_files = {self._guess_run_from_file(file): file for file in input_file_list}
+        elif self.simulation_software in ["corsika", "corsika_simtel"]:
+            _runs_and_files = {run: None for run in self._get_runs_to_simulate()}
+        if len(_runs_and_files) == 0:
+            raise ValueError("No runs to submit.")
         return _runs_and_files
 
     @staticmethod
     def _enforce_list_type(input_file_list):
-        """Enforce the input list to be a list."""
+        """
+        Enforce the input list to be a list.
+
+        Parameters
+        ----------
+        input_file_list: str or list of str
+            Single file or list of files of shower simulations.
+
+        Returns
+        -------
+        list
+            List of input files.
+        """
         if not input_file_list:
             return []
-        if not isinstance(input_file_list, list):
-            return [input_file_list]
-
-        return input_file_list
+        return input_file_list if isinstance(input_file_list, list) else [input_file_list]
 
     def _guess_run_from_file(self, file):
         """
@@ -491,42 +360,45 @@ def _guess_run_from_file(self, file):
             run_str = re.search(r"run\d*", file_name).group()
             return int(run_str[3:])
         except (ValueError, AttributeError):
-            msg = f"Run number could not be guessed from {file_name} using run = 1"
-            self._logger.warning(msg)
+            self._logger.warning(f"Run number could not be guessed from {file_name} using run = 1")
             return 1
 
-    def _fill_results(self, file, run):
+    def _fill_results(self, file, run_number):
         """
-        Fill the results dict with input, output and log files.
+        Fill the results dict with input, output, hist, and log files.
 
         Parameters
         ----------
         file: str
             input file name
-        run: int
+        run_number: int
             run number
 
         """
-        info_for_file_name = self._simulation_runner.get_info_for_file_name(run)
         self._results["output"].append(
-            str(self._simulation_runner.get_file_name(file_type="output", **info_for_file_name))
+            str(self._simulation_runner.get_file_name(file_type="output", run_number=run_number))
         )
         self._results["sub_out"].append(
             str(
                 self._simulation_runner.get_file_name(
-                    file_type="sub_log", **info_for_file_name, mode="out"
+                    file_type="sub_log", run_number=run_number, mode="out"
                 )
             )
         )
+
         if self.simulation_software in ["simtel", "corsika_simtel"]:
             self._results["log"].append(
-                str(self._simulation_runner.get_file_name(file_type="log", **info_for_file_name))
+                str(
+                    self._simulation_runner.get_file_name(
+                        simulation_software="simtel", file_type="log", run_number=run_number
+                    )
+                )
             )
             self._results["input"].append(str(file))
             self._results["hist"].append(
                 str(
                     self._simulation_runner.get_file_name(
-                        file_type="histogram", **info_for_file_name
+                        simulation_software="simtel", file_type="histogram", run_number=run_number
                     )
                 )
             )
@@ -534,7 +406,7 @@ def _fill_results(self, file, run):
             self._results["corsika_autoinputs_log"].append(
                 str(
                     self._simulation_runner.get_file_name(
-                        file_type="corsika_autoinputs_log", **info_for_file_name
+                        simulation_software="corsika", file_type="log", run_number=run_number
                     )
                 )
             )
@@ -542,103 +414,47 @@ def _fill_results(self, file, run):
             self._results["hist"].append(None)
             self._results["log"].append(None)
 
-    def get_list_of_output_files(self, run_list=None, run_range=None):
+    def get_file_list(self, file_type="output"):
         """
-        Get list of output files.
+        Get list of files generated by simulations.
+
+        Options are "input", "output", "hist", "log".
+        Not all file types are available for all simulation types.
+        Returns an empty list for an unknown file type.
 
         Parameters
         ----------
-        run_list: list
-            List of run numbers.
-        run_range: list
-            List of len 2 with the limits of the range of the run numbers.
+        file_type : str
+            File type to be listed.
 
         Returns
         -------
         list
-            List with the full path of all the output files.
+            List with the full path of all output files.
 
         """
-        self._logger.info("Getting list of output files")
+        self._logger.info(f"Getting list of {file_type} files")
+        return self._results[file_type]
 
-        if run_list or run_range or len(self._results["output"]) == 0:
-            runs_to_list = self._get_runs_to_simulate(run_list=run_list, run_range=run_range)
-
-            for run in runs_to_list:
-                output_file_name = self._simulation_runner.get_file_name(
-                    file_type="output", **self._simulation_runner.get_info_for_file_name(run)
-                )
-                self._results["output"].append(str(output_file_name))
-        return self._results["output"]
-
-    def get_list_of_histogram_files(self):
+    def print_list_of_files(self, file_type="output"):
         """
-        Get list of histogram files.
+        Print list of output files generated by simulations.
 
-        (not applicable to all simulation types)
+        Options are "input", "output", "hist", "log".
 
-        Returns
-        -------
-        list
-            List with the full path of all the histogram files.
-        """
-        self._logger.info("Getting list of histogram files")
-        return self._results["hist"]
-
-    def get_list_of_input_files(self):
-        """
-        Get list of input files.
-
-        Returns
-        -------
-        list
-            List with the full path of all the input files.
-        """
-        self._logger.info("Getting list of input files")
-        return self._results["input"]
-
-    def get_list_of_log_files(self):
-        """
-        Get list of log files.
+        Parameters
+        ----------
+        file_type : str
+            File type to be listed.
 
-        Returns
-        -------
-        list
-            List with the full path of all the log files.
         """
-        self._logger.info("Getting list of log files")
-        if self.simulation_software in ["simtel", "corsika_simtel"]:
-            return self._results["log"]
-        return self._results["corsika_autoinputs_log"]
-
-    def print_list_of_output_files(self):
-        """Print list of output files."""
-        self._logger.info("Printing list of output files")
-        self._print_list_of_files(which="output")
-
-    def print_list_of_histogram_files(self):
-        """Print list of histogram files."""
-        self._logger.info("Printing list of histogram files")
-        self._print_list_of_files(which="hist")
-
-    def print_list_of_input_files(self):
-        """Print list of output files."""
-        self._logger.info("Printing list of input files")
-        self._print_list_of_files(which="input")
-
-    def print_list_of_log_files(self):
-        """Print list of log files."""
-        self._logger.info("Printing list of log files")
-        if self.simulation_software in ["simtel", "corsika_simtel"]:
-            self._print_list_of_files(which="log")
-        else:
-            self._print_list_of_files(which="corsika_autoinputs_log")
+        self._logger.info(f"Printing list of {file_type} files")
+        for file in self._results[file_type]:
+            print(file)
 
     def _make_resources_report(self, input_file_list):
         """
-        Prepare a simple report on computing resources used.
-
-        Includes wall clock time per run only at this point.
+        Prepare a simple report on computing wall clock time used in the simulations.
 
         Parameters
         ----------
@@ -653,7 +469,7 @@ def _make_resources_report(self, input_file_list):
         """
         if len(self._results["sub_out"]) == 0:
             if input_file_list is None:
-                return {"Walltime/run [sec]": np.nan}
+                return {"Wall time/run [sec]": np.nan}
             self._fill_results_without_run(input_file_list)
 
         runtime = []
@@ -667,10 +483,10 @@ def _make_resources_report(self, input_file_list):
         mean_runtime = np.mean(runtime)
 
         resource_summary = {}
-        resource_summary["Walltime/run [sec]"] = mean_runtime
+        resource_summary["Wall time/run [sec]"] = mean_runtime
         if "n_events" in _resources and _resources["n_events"] > 0:
             resource_summary["#events/run"] = _resources["n_events"]
-            resource_summary["Walltime/1000 events [sec]"] = (
+            resource_summary["Wall time/1000 events [sec]"] = (
                 mean_runtime * 1000 / _resources["n_events"]
             )
 
@@ -713,27 +529,5 @@ def _get_runs_to_simulate(self, run_list=None, run_range=None):
 
         """
         if run_list is None and run_range is None:
-            if self.runs is None:
-                msg = "Runs to simulate were not given as arguments nor in config_data - aborting"
-                self._logger.error(msg)
-                return []
-
-            return self.runs
-
+            return [] if self.runs is None else self.runs
         return self._validate_run_list_and_range(run_list, run_range)
-
-    def _print_list_of_files(self, which):
-        """
-        Print list of files of a certain type.
-
-        Parameters
-        ----------
-        which str
-            file type (e.g., log)
-
-        """
-        if which not in self._results:
-            self._logger.error(f"Invalid file type {which}")
-            raise KeyError
-        for file in self._results[which]:
-            print(file)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 5ecd58a7a..84fda80cd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,10 +11,12 @@
 
 import simtools.io_operations.io_handler
 from simtools.configuration.configurator import Configurator
+from simtools.corsika.corsika_config import CorsikaConfig
 from simtools.db import db_handler
 from simtools.model.array_model import ArrayModel
 from simtools.model.site_model import SiteModel
 from simtools.model.telescope_model import TelescopeModel
+from simtools.runners.corsika_runner import CorsikaRunner
 
 logger = logging.getLogger()
 
@@ -176,24 +178,24 @@ def model_version():
 
 
 @pytest.fixture()
-def array_model_north(simulator_config_data_north, io_handler, db_config, model_version):
+def array_model_north(io_handler, db_config, model_version):
     """Array model for North site."""
     return ArrayModel(
         label="test-lst-array",
-        site=simulator_config_data_north["common"]["site"],
-        layout_name=simulator_config_data_north["common"]["layout_name"],
+        site="North",
+        layout_name="test_layout",
         mongo_db_config=db_config,
         model_version=model_version,
     )
 
 
 @pytest.fixture()
-def array_model_south(simulator_config_data_south, io_handler, db_config, model_version):
+def array_model_south(io_handler, db_config, model_version):
     """Array model for South site."""
     return ArrayModel(
         label="test-lst-array",
-        site=simulator_config_data_south["common"]["site"],
-        layout_name=simulator_config_data_south["common"]["layout_name"],
+        site="South",
+        layout_name="test_layout",
         mongo_db_config=db_config,
         model_version=model_version,
     )
@@ -318,61 +320,44 @@ def corsika_histograms_instance_set_histograms(db, io_handler, corsika_histogram
 
 
 @pytest.fixture()
-def simulator_config_data_north(tmp_test_directory):
+def corsika_config_data():
+    """Corsika configuration data (as given by CorsikaConfig)."""
     return {
-        "common": {
-            "site": "North",
-            "layout_name": "test_layout",
-            "data_directory": f"{tmp_test_directory!s}/test-output",
-            "zenith": 20 * u.deg,
-            "azimuth": 0 * u.deg,
-            "primary": "gamma",
-        },
-        "showers": {
-            "eslope": -2.5,
-            "viewcone": [0 * u.deg, 0 * u.deg],
-            "nshow": 10,
-            "erange": [100 * u.GeV, 1 * u.TeV],
-            "cscat": [10, 1400 * u.m, 0],
-            "run_list": [3, 4],
-            "run_range": [6, 10],
-        },
-        "array": {},
+        "nshow": 100,
+        "run_number_start": 0,
+        "number_of_runs": 10,
+        "event_number_first_shower": 1,
+        "zenith_angle": 20 * u.deg,
+        "azimuth_angle": 0.0 * u.deg,
+        "viewcone": (0.0 * u.deg, 5.0 * u.deg),
+        "erange": (10.0 * u.GeV, 10.0 * u.TeV),
+        "eslope": -2,
+        "core_scatter": (10, 1400.0 * u.m),
+        "primary": "proton",
+        "data_directory": "simtools-output",
     }
 
 
 @pytest.fixture()
-def array_config_data(simulator_config_data):
-    return simulator_config_data["common"] | simulator_config_data["array"]
-
-
-@pytest.fixture()
-def shower_config_data_north(simulator_config_data_north):
-    return simulator_config_data_north["common"] | simulator_config_data_north["showers"]
+def corsika_config(io_handler, corsika_config_data, array_model_south):
+    """Corsika configuration object (using array model South)."""
+    corsika_config = CorsikaConfig(
+        array_model=array_model_south,
+        label="test-corsika-config",
+        args_dict=corsika_config_data,
+    )
+    corsika_config.run_number = 1
+    return corsika_config
 
 
 @pytest.fixture()
-def simulator_config_data_south(tmp_test_directory):
-    return {
-        "common": {
-            "site": "South",
-            "layout_name": "test_layout",
-            "data_directory": f"{tmp_test_directory!s}/test-output",
-            "zenith": 20 * u.deg,
-            "azimuth": 0 * u.deg,
-            "primary": "gamma",
-        },
-        "showers": {
-            "eslope": -2.5,
-            "viewcone": [0 * u.deg, 0 * u.deg],
-            "nshow": 10,
-            "erange": [100 * u.GeV, 1 * u.TeV],
-            "cscat": [10, 1400 * u.m, 0],
-            "run_list": [3, 4],
-            "run_range": [6, 10],
-        },
-        "array": {},
-    }
+def corsika_runner(corsika_config, io_handler, simtel_path):
+    return CorsikaRunner(
+        corsika_config=corsika_config,
+        simtel_path=simtel_path,
+        label="test-corsika-runner",
+        use_multipipe=False,
+    )
 
 
 @pytest.fixture()
diff --git a/tests/integration_tests/config/derive_psf_parameters_run.yml b/tests/integration_tests/config/derive_psf_parameters_run.yml
index d79666703..492ae0007 100644
--- a/tests/integration_tests/config/derive_psf_parameters_run.yml
+++ b/tests/integration_tests/config/derive_psf_parameters_run.yml
@@ -10,4 +10,4 @@ CTA_SIMPIPE:
     TEST: true
     OUTPUT_PATH: simtools-tests
   INTEGRATION_TESTS:
-    - OUTPUT_FILE: ray-tracing/results/ray-tracing-North-LSTN-01-d10.0km-za20.0deg_derive_psf_parameters.ecsv
+    - OUTPUT_FILE: ray-tracing/results/ray-tracing-North-LSTN-01-d10.0km-za20.0deg_tune_psf.ecsv
diff --git a/tests/integration_tests/config/plot_array_layout_two_files.yml b/tests/integration_tests/config/plot_array_layout_two_files.yml
deleted file mode 100644
index 9d9d75c5f..000000000
--- a/tests/integration_tests/config/plot_array_layout_two_files.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-CTA_SIMPIPE:
-  APPLICATION: simtools-plot-array-layout
-  TEST_NAME: two_files
-  CONFIGURATION:
-    ARRAY_LAYOUT_FILE:
-      - tests/resources/telescope_positions-North-ground.ecsv
-      - tests/resources/telescope_positions-South-ground.ecsv
-    MODEL_VERSION: prod6
-    OUTPUT_PATH: simtools-tests
-  INTEGRATION_TESTS:
-    - OUTPUT_FILE: application-plots/array_layout_telescope_positions-North-ground_North_ground_0deg.pdf
-    - OUTPUT_FILE: application-plots/array_layout_telescope_positions-North-ground_North_ground_0deg.png
-    - OUTPUT_FILE: application-plots/array_layout_telescope_positions-South-ground_South_ground_0deg.pdf
-    - OUTPUT_FILE: application-plots/array_layout_telescope_positions-South-ground_South_ground_0deg.png
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_North.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_North.yml
index 4c5aa0247..01fb28587 100644
--- a/tests/integration_tests/config/simulate_prod_gamma_20_deg_North.yml
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_North.yml
@@ -2,21 +2,28 @@ CTA_SIMPIPE:
   APPLICATION: simtools-simulate-prod
   TEST_NAME: gamma_20_deg_north
   CONFIGURATION:
-    PRODUCTION_CONFIG: ./tests/resources/prod_multi_config_test_North.yml
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-North
     MODEL_VERSION: prod6
     SITE: North
+    ARRAY_LAYOUT_NAME: test_layout
     PRIMARY: gamma
+    RUN_NUMBER_START: 10
+    NUMBER_OF_RUNS: 1
     AZIMUTH_ANGLE: North
     ZENITH_ANGLE: 20
-    START_RUN: 0
-    RUN: 2
-    NSHOW: 5
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 500 m"
     DATA_DIRECTORY: simtools-output
     OUTPUT_PATH: simtools-output
+    LOG_LEVEL: DEBUG
   INTEGRATION_TESTS:
     - OUTPUT_FILE: |
-        test-production-North/simtel-data/North/gamma/data/run000002_gamma_za020deg_azm000deg_North_test_layout_test-production-North.simtel.zst
+        simtel/data/run000010/run000010_gamma_za20deg_azm000deg_North_test_layout_test-production-North.zst
     - OUTPUT_FILE: |
-        test-production-North/simtel-data/North/gamma/log/run000002_gamma_za020deg_azm000deg_North_test_layout_test-production-North.log.gz
+        simtel/logs/run000010_gamma_za20deg_azm000deg_North_test_layout_test-production-North.log.gz
     - OUTPUT_FILE: |
-        test-production-North/simtel-data/North/gamma/log/run000002_gamma_za020deg_azm000deg_North_test_layout_test-production-North.hdata.zst
+        simtel/logs/run000010_gamma_za20deg_azm000deg_North_test_layout_test-production-North.hdata.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_corsika.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_corsika.yml
index 51d553178..1e2a3395f 100644
--- a/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_corsika.yml
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_corsika.yml
@@ -2,20 +2,28 @@ CTA_SIMPIPE:
   APPLICATION: simtools-simulate-prod
   TEST_NAME: gamma_20_deg_corsika
   CONFIGURATION:
-    PRODUCTION_CONFIG: ./tests/resources/prod_multi_config_test_North.yml
     SIMULATION_SOFTWARE: corsika
+    LABEL: test-production-North-corsika
     MODEL_VERSION: prod6
     SITE: North
+    ARRAY_LAYOUT_NAME: test_layout
     PRIMARY: gamma
+    RUN_NUMBER_START: 10
+    NUMBER_OF_RUNS: 2
     AZIMUTH_ANGLE: North
     ZENITH_ANGLE: 20
-    START_RUN: 0
-    RUN: 2
-    NSHOW: 5
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 2 TeV"
+    CORE_SCATTER: "10 1400 m"
     DATA_DIRECTORY: simtools-output
     OUTPUT_PATH: simtools-output
+    LOG_LEVEL: DEBUG
   INTEGRATION_TESTS:
     - OUTPUT_FILE: |
-        test-production-North/corsika-data/North/gamma/data/run000002/corsika_run000002_gamma_za020deg_azm000deg_North_test_layout_test-production-North.zst
+        corsika/data/run000010/run000010_gamma_North_test_layout_za020-azm000deg_test-production-North-corsika_North_test_layout_test-production-North-corsika.zst
     - OUTPUT_FILE: |
-        test-production-North/corsika-data/North/gamma/data/run000002/run2.log
+        corsika/data/run000011/run000011_gamma_North_test_layout_za020-azm000deg_test-production-North-corsika_North_test_layout_test-production-North-corsika.zst
+    - OUTPUT_FILE: |
+        corsika/data/run000010/run10.log
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_for_trigger_rates.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_for_trigger_rates.yml
new file mode 100644
index 000000000..50844fdd4
--- /dev/null
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_North_for_trigger_rates.yml
@@ -0,0 +1,29 @@
+CTA_SIMPIPE:
+  APPLICATION: simtools-simulate-prod
+  TEST_NAME: gamma_20_deg_north_for_trigger_rates
+  CONFIGURATION:
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-North-for-trigger-rates
+    MODEL_VERSION: prod6
+    SITE: North
+    ARRAY_LAYOUT_NAME: subsystem_lsts
+    PRIMARY: proton
+    RUN_NUMBER_START: 1
+    NUMBER_OF_RUNS: 2
+    AZIMUTH_ANGLE: North
+    ZENITH_ANGLE: 20
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 500 m"
+    DATA_DIRECTORY: simtools-output
+    OUTPUT_PATH: simtools-output
+    LOG_LEVEL: DEBUG
+  INTEGRATION_TESTS:
+    - OUTPUT_FILE: |
+        simtel/data/run000001/run000001_proton_za20deg_azm000deg_North_subsystem_lsts_test-production-North-for-trigger-rates.zst
+    - OUTPUT_FILE: |
+        simtel/logs/run000001_proton_za20deg_azm000deg_North_subsystem_lsts_test-production-North-for-trigger-rates.log.gz
+    - OUTPUT_FILE: |
+        simtel/logs/run000001_proton_za20deg_azm000deg_North_subsystem_lsts_test-production-North-for-trigger-rates.hdata.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_South.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_South.yml
index d4430427b..5bd1486b5 100644
--- a/tests/integration_tests/config/simulate_prod_gamma_20_deg_South.yml
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_South.yml
@@ -2,21 +2,28 @@ CTA_SIMPIPE:
   APPLICATION: simtools-simulate-prod
   TEST_NAME: gamma_20_deg_south
   CONFIGURATION:
-    PRODUCTION_CONFIG: ./tests/resources/prod_multi_config_test_South.yml
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-South
     MODEL_VERSION: prod6
     SITE: South
+    ARRAY_LAYOUT_NAME: test_layout
     PRIMARY: gamma
+    RUN_NUMBER_START: 10
+    NUMBER_OF_RUNS: 1
     AZIMUTH_ANGLE: North
     ZENITH_ANGLE: 20
-    START_RUN: 0
-    RUN: 2
-    NSHOW: 2
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 1400 m"
     DATA_DIRECTORY: simtools-output
     OUTPUT_PATH: simtools-output
+    LOG_LEVEL: DEBUG
   INTEGRATION_TESTS:
     - OUTPUT_FILE: |
-        test-production-South/simtel-data/South/gamma/data/run000002_gamma_za020deg_azm000deg_South_test_layout_test-production-South.simtel.zst
+        simtel/data/run000010/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South.zst
     - OUTPUT_FILE: |
-        test-production-South/simtel-data/South/gamma/log/run000002_gamma_za020deg_azm000deg_South_test_layout_test-production-South.log.gz
+        simtel/logs/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South.log.gz
     - OUTPUT_FILE: |
-        test-production-South/simtel-data/South/gamma/log/run000002_gamma_za020deg_azm000deg_South_test_layout_test-production-South.hdata.zst
+        simtel/logs/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South.hdata.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_South_for_trigger_rates.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_South_for_trigger_rates.yml
new file mode 100644
index 000000000..db741db04
--- /dev/null
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_South_for_trigger_rates.yml
@@ -0,0 +1,29 @@
+CTA_SIMPIPE:
+  APPLICATION: simtools-simulate-prod
+  TEST_NAME: gamma_20_deg_north_for_trigger_rates
+  CONFIGURATION:
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-South-for-trigger-rates
+    MODEL_VERSION: prod6
+    SITE: South
+    ARRAY_LAYOUT_NAME: 4mst
+    PRIMARY: proton
+    RUN_NUMBER_START: 1
+    NUMBER_OF_RUNS: 2
+    AZIMUTH_ANGLE: North
+    ZENITH_ANGLE: 20
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 500 m"
+    DATA_DIRECTORY: simtools-output
+    OUTPUT_PATH: simtools-output
+    LOG_LEVEL: DEBUG
+  INTEGRATION_TESTS:
+    - OUTPUT_FILE: |
+        simtel/data/run000001/run000001_proton_za20deg_azm000deg_South_4mst_test-production-South-for-trigger-rates.zst
+    - OUTPUT_FILE: |
+        simtel/logs/run000001_proton_za20deg_azm000deg_South_4mst_test-production-South-for-trigger-rates.log.gz
+    - OUTPUT_FILE: |
+        simtel/logs/run000001_proton_za20deg_azm000deg_South_4mst_test-production-South-for-trigger-rates.hdata.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid.yml
deleted file mode 100644
index 6b5e899b2..000000000
--- a/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-CTA_SIMPIPE:
-  APPLICATION: simtools-simulate-prod
-  TEST_NAME: gamma_20_deg_pack_for_grid
-  CONFIGURATION:
-    PRODUCTION_CONFIG: ./tests/resources/prod_multi_config_test_North.yml
-    MODEL_VERSION: prod6
-    SITE: North
-    PRIMARY: gamma
-    AZIMUTH_ANGLE: North
-    ZENITH_ANGLE: 20
-    START_RUN: 0
-    RUN: 2
-    NSHOW: 5
-    DATA_DIRECTORY: simtools-output
-    OUTPUT_PATH: simtools-output
-    PACK_FOR_GRID_REGISTER: true
-  INTEGRATION_TESTS:
-    - OUTPUT_FILE: |
-        directory_for_grid_upload/run000002_gamma_za020deg_azm000deg_North_test_layout_test-production-North.log_hist.tar.gz
-    - OUTPUT_FILE: |
-        directory_for_grid_upload/run000002_gamma_za020deg_azm000deg_North_test_layout_test-production-North.simtel.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_North.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_North.yml
new file mode 100644
index 000000000..37d8b531d
--- /dev/null
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_North.yml
@@ -0,0 +1,28 @@
+CTA_SIMPIPE:
+  APPLICATION: simtools-simulate-prod
+  TEST_NAME: gamma_20_deg_north_pack_for_grid
+  CONFIGURATION:
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-North-pack-for-grid
+    MODEL_VERSION: prod6
+    SITE: North
+    ARRAY_LAYOUT_NAME: test_layout
+    PRIMARY: gamma
+    RUN_NUMBER_START: 10
+    NUMBER_OF_RUNS: 1
+    AZIMUTH_ANGLE: North
+    ZENITH_ANGLE: 20
+    NSHOW: 5
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 500 m"
+    DATA_DIRECTORY: simtools-output
+    OUTPUT_PATH: simtools-output
+    PACK_FOR_GRID_REGISTER: true
+    LOG_LEVEL: DEBUG
+  INTEGRATION_TESTS:
+    - OUTPUT_FILE: |
+        directory_for_grid_upload/run000010_gamma_za20deg_azm000deg_North_test_layout_test-production-North-pack-for-grid.log_hist.tar.gz
+    - OUTPUT_FILE: |
+        directory_for_grid_upload/run000010_gamma_za20deg_azm000deg_North_test_layout_test-production-North-pack-for-grid.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_South.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_South.yml
new file mode 100644
index 000000000..c818f2f34
--- /dev/null
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_South.yml
@@ -0,0 +1,27 @@
+CTA_SIMPIPE:
+  APPLICATION: simtools-simulate-prod
+  TEST_NAME: gamma_20_deg_pack_for_south_grid
+  CONFIGURATION:
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-South-pack-for-grid
+    MODEL_VERSION: prod6
+    SITE: South
+    ARRAY_LAYOUT_NAME: test_layout
+    PRIMARY: gamma
+    RUN_NUMBER_START: 10
+    NUMBER_OF_RUNS: 1
+    AZIMUTH_ANGLE: North
+    ZENITH_ANGLE: 20
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 1400 m"
+    DATA_DIRECTORY: simtools-output
+    OUTPUT_PATH: simtools-output
+    PACK_FOR_GRID_REGISTER: true
+  INTEGRATION_TESTS:
+    - OUTPUT_FILE: |
+        directory_for_grid_upload/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South-pack-for-grid.log_hist.tar.gz
+    - OUTPUT_FILE: |
+        directory_for_grid_upload/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South-pack-for-grid.zst
diff --git a/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_south.yml b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_south.yml
index ff7be60f2..c818f2f34 100644
--- a/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_south.yml
+++ b/tests/integration_tests/config/simulate_prod_gamma_20_deg_pack_for_grid_south.yml
@@ -1,21 +1,27 @@
 CTA_SIMPIPE:
   APPLICATION: simtools-simulate-prod
-  TEST_NAME: gamma_20_deg_pack_for_grid_south
+  TEST_NAME: gamma_20_deg_pack_for_south_grid
   CONFIGURATION:
-    PRODUCTION_CONFIG: ./tests/resources/prod_multi_config_test_South.yml
+    SIMULATION_SOFTWARE: corsika_simtel
+    LABEL: test-production-South-pack-for-grid
     MODEL_VERSION: prod6
     SITE: South
+    ARRAY_LAYOUT_NAME: test_layout
     PRIMARY: gamma
+    RUN_NUMBER_START: 10
+    NUMBER_OF_RUNS: 1
     AZIMUTH_ANGLE: North
     ZENITH_ANGLE: 20
-    START_RUN: 0
-    RUN: 2
-    NSHOW: 5
+    NSHOW: 10
+    ESLOPE: -2.
+    VIEWCONE: "0 deg 0 deg"
+    ERANGE: "100 GeV 1 TeV"
+    CORE_SCATTER: "10 1400 m"
     DATA_DIRECTORY: simtools-output
     OUTPUT_PATH: simtools-output
     PACK_FOR_GRID_REGISTER: true
   INTEGRATION_TESTS:
     - OUTPUT_FILE: |
-        directory_for_grid_upload/run000002_gamma_za020deg_azm000deg_South_test_layout_test-production-South.log_hist.tar.gz
+        directory_for_grid_upload/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South-pack-for-grid.log_hist.tar.gz
     - OUTPUT_FILE: |
-        directory_for_grid_upload/run000002_gamma_za020deg_azm000deg_South_test_layout_test-production-South.simtel.zst
+        directory_for_grid_upload/run000010_gamma_za20deg_azm000deg_South_test_layout_test-production-South-pack-for-grid.zst
diff --git a/tests/integration_tests/config/simulate_showers_for_trigger_rates_run.yml b/tests/integration_tests/config/simulate_showers_for_trigger_rates_run.yml
deleted file mode 100644
index 60830eebd..000000000
--- a/tests/integration_tests/config/simulate_showers_for_trigger_rates_run.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-CTA_SIMPIPE:
-  APPLICATION: simtools-simulate-showers-for-trigger-rates
-  TEST_NAME: run
-  CONFIGURATION:
-    ARRAY_LAYOUT_NAME: subsystem_lsts
-    SITE: North
-    PRIMARY: proton
-    MODEL_VERSION: prod6
-    NRUNS: 2
-    NEVENTS: 2
-    SUBMIT_COMMAND: local
-    DATA_DIRECTORY: simtools-output
-    OUTPUT_PATH: simtools-output
-  INTEGRATION_TESTS:
-    - OUTPUT_FILE: |
-        simulate_showers_for_trigger_rates/simtel-data/North/proton/data/run000001_proton_za020deg_azm000deg_North_subsystem_lsts_simulate_showers_for_trigger_rates.simtel.zst
diff --git a/tests/integration_tests/config/simulate_showers_for_trigger_rates_run_South.yml b/tests/integration_tests/config/simulate_showers_for_trigger_rates_run_South.yml
deleted file mode 100644
index 07de58564..000000000
--- a/tests/integration_tests/config/simulate_showers_for_trigger_rates_run_South.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-CTA_SIMPIPE:
-  APPLICATION: simtools-simulate-showers-for-trigger-rates
-  TEST_NAME: run_south
-  CONFIGURATION:
-    ARRAY: 4MST
-    SITE: South
-    PRIMARY: proton
-    MODEL_VERSION: prod6
-    NRUNS: 2
-    NEVENTS: 2
-    SUBMIT_COMMAND: local
-    DATA_DIRECTORY: simtools-output
-    OUTPUT_PATH: simtools-output
-  INTEGRATION_TESTS:
-    - OUTPUT_FILE: |
-        simulate_showers_for_trigger_rates/simtel-data/South/proton/data/run000001_proton_za020deg_azm000deg_South_4MST_simulate_showers_for_trigger_rates.simtel.zst
diff --git a/tests/resources/array_config_test.yml b/tests/resources/array_config_test.yml
deleted file mode 100644
index 8d9b8e530..000000000
--- a/tests/resources/array_config_test.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-common:
-  site: North
-  layout_name: test_layout
-array:
-  model_version: Prod5
-  default:
-      LSTN: 'design'
-      MSTN: 'design'
-  LSTN-01:
-      name: "01"
-  LSTN-02:
-      name: "02"
-  LSTN-03:
-      name: "03"
-  LSTN-04:
-      name: "04"
-  MSTN-05:
-      name: 'design'
-      fadc_pulse_shape: Pulse_template_nectarCam_17042020-noshift.dat
-      discriminator_pulse_shape: Pulse_template_nectarCam_17042020-noshift.dat
diff --git a/tests/resources/corsikaConfigTest.yml b/tests/resources/corsikaConfigTest.yml
deleted file mode 100644
index 2dbe76451..000000000
--- a/tests/resources/corsikaConfigTest.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-# Run number
-nrun: 10
-
-# Number of showers per run
-nshow: 100
-
-# Primary particle name - proton, gamma, ...
-primary: 'proton'
-
-# Energy range - [emin, emax]
-erange:
-  - unit: 'GeV'
-    value: 0.01
-  - unit: 'GeV'
-    value: 10.0
-
-# Slope of energy spectrum
-eslope: -2
-
-# Zenith angle - single value or list of len 2 for range with min and max zenith
-zenith:
-  unit: 'deg'
-  value: 20.0
-
-# Azimuth angle - single value or list of len 2 for range with min and max azimuth
-phi:
-  unit: 'deg'
-  value: 0.0
-
-# CSCAT parameters - list of len 3
-# First entry is the number of uses of each shower
-# Second entry is the scattering radius in units of distance
-# Third entry is 0, for circlar scattering area
-cscat:
-  - 10
-  - unit: 'm'
-    value: 1500.0
-  - 0
-viewcone:
-  unit: 'deg'
-  value: 5.0
diff --git a/tests/resources/prod_multi_config_test_North.yml b/tests/resources/prod_multi_config_test_North.yml
deleted file mode 100644
index a2a093c9c..000000000
--- a/tests/resources/prod_multi_config_test_North.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-common:
-  # Make sure to include "divergent/convergent" in the label in case that is the pointing strategy. This tells sim_telarray not to overwrite the telescope pointings
-  label: 'test-production-North'
-  site: North
-  layout_name: test_layout
-  data_directory: ./
-  zenith: 20 deg
-  phi: 0 deg
-  primary: gamma
-showers:
-  eslope: -2.0
-  viewcone: [0 deg, 0 deg]
-  nshow: 10
-  erange: [3 GeV, 330 TeV]
-  cscat: [10, 1400.0 m, 0]
diff --git a/tests/resources/prod_multi_config_test_South.yml b/tests/resources/prod_multi_config_test_South.yml
deleted file mode 100644
index d6082e4b7..000000000
--- a/tests/resources/prod_multi_config_test_South.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-common:
-  # Make sure to include "divergent/convergent" in the label in case that is the pointing strategy. This tells sim_telarray not to overwrite the telescope pointings
-  label: 'test-production-South'
-  site: South
-  layout_name: test_layout
-  data_directory: ./
-  zenith: 20 deg
-  phi: 0 deg
-  primary: gamma
-showers:
-  eslope: -2.0
-  viewcone: [0 deg, 0 deg]
-  nshow: 10
-  erange: [3 GeV, 330 TeV]
-  cscat: [10, 1400.0 m, 0]
-array:
-  model_version: Prod5
-  default:
-    LSTS: 'design'
-    MSTS: 'design'
-    SSTS: 'design'
diff --git a/tests/unit_tests/corsika/test_corsika_config.py b/tests/unit_tests/corsika/test_corsika_config.py
index 5fa0c0bbb..1a434b5f9 100644
--- a/tests/unit_tests/corsika/test_corsika_config.py
+++ b/tests/unit_tests/corsika/test_corsika_config.py
@@ -1,221 +1,219 @@
 #!/usr/bin/python3
 
 import logging
-from copy import copy
+import pathlib
+from unittest.mock import Mock, patch
 
 import pytest
-from astropy import units as u
 
-from simtools.corsika.corsika_config import (
-    CorsikaConfig,
-    InvalidCorsikaInputError,
-    MissingRequiredInputInCorsikaConfigDataError,
-)
+from simtools.corsika.corsika_config import CorsikaConfig, InvalidCorsikaInputError
 
 logger = logging.getLogger()
 logger.setLevel(logging.DEBUG)
 
 
-@pytest.fixture()
-def corsika_config_data():
-    return {
-        "nshow": 100,
-        "nrun": 10,
-        "zenith": 20 * u.deg,
-        "viewcone": 5 * u.deg,
-        "erange": [10 * u.GeV, 10 * u.TeV],
-        "eslope": -2,
-        "phi": 0 * u.deg,
-        "cscat": [10, 1500 * u.m, 0],
-        "primary": "proton",
-    }
-
-
-@pytest.fixture()
-def corsika_config(io_handler, corsika_config_data, array_model_south):
-    return CorsikaConfig(
-        array_model=array_model_south,
-        label="test-corsika-config",
-        corsika_config_data=corsika_config_data,
-    )
-
-
 def test_repr(corsika_config):
-    logger.info("test_repr")
-    text = repr(corsika_config)
+    assert "site" in repr(corsika_config)
 
-    assert "site" in text
 
+def test_load_corsika_default_parameters_file(io_handler):
+    cc = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=None,
+    )
+    corsika_parameters = cc._load_corsika_default_parameters_file()
+    assert isinstance(corsika_parameters, dict)
+    assert "CHERENKOV_EMISSION_PARAMETERS" in corsika_parameters
 
-def test_user_parameters(corsika_config):
-    logger.info("test_user_parameters")
 
-    assert corsika_config.get_user_parameter("nshow") == 100
-    assert corsika_config.get_user_parameter("thetap") == [20, 20]
-    assert corsika_config.get_user_parameter("erange") == [10.0, 10000.0]
+def test_setup_configuration(io_handler, corsika_config_data):
+    cc = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=corsika_config_data,
+    )
+    assert cc.get_config_parameter("NSHOW") == 100
+    assert cc.get_config_parameter("THETAP") == [20, 20]
+    assert cc.get_config_parameter("ERANGE") == [10.0, 10000.0]
     # Testing conversion between AZM (sim_telarray) and PHIP (corsika)
-    assert corsika_config.get_user_parameter("azm") == [0.0, 0.0]
-    assert corsika_config.get_user_parameter("phip") == [180.0, 180.0]
+    assert cc.get_config_parameter("PHIP") == [180.0, 180.0]
+    assert cc.get_config_parameter("CSCAT") == [10, 140000.0, 0]
 
-    with pytest.raises(KeyError):
-        corsika_config.get_user_parameter("inexistent_par")
+    assert isinstance(cc.setup_configuration(), dict)
+    cc.args_dict = None
+    assert cc.setup_configuration() is None
 
 
-def test_export_input_file(corsika_config):
-    logger.info("test_export_input_file")
-    corsika_config.export_input_file()
-    input_file = corsika_config.get_input_file()
-    assert input_file.exists()
-    with open(input_file) as f:
-        assert "TELFIL |" not in f.read()
+def test_rotate_azimuth_by_180deg(io_handler):
+    cc = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=None,
+    )
+    assert pytest.approx(cc._rotate_azimuth_by_180deg(0.0)) == 180.0
+    assert pytest.approx(cc._rotate_azimuth_by_180deg(360.0)) == 180.0
+    assert pytest.approx(cc._rotate_azimuth_by_180deg(450.0)) == 270.0
+    assert pytest.approx(cc._rotate_azimuth_by_180deg(180.0)) == 0.0
+    assert pytest.approx(cc._rotate_azimuth_by_180deg(-180.0)) == 0.0
 
 
-def test_export_input_file_multipipe(corsika_config):
-    logger.info("test_export_input_file")
-    corsika_config.export_input_file(use_multipipe=True)
-    input_file = corsika_config.get_input_file()
-    assert input_file.exists()
-    with open(input_file) as f:
-        assert "TELFIL |" in f.read()
-
+def test_convert_primary_input_and_store_primary_name(io_handler):
+    cc = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=None,
+    )
+    assert cc._convert_primary_input_and_store_primary_name("Gamma") == 1
+    assert cc._convert_primary_input_and_store_primary_name("proton") == 14
+    assert cc._convert_primary_input_and_store_primary_name("Helium") == 402
+    assert cc._convert_primary_input_and_store_primary_name("IRON") == 5626
 
-def test_wrong_par_in_config_data(corsika_config, corsika_config_data, array_model_north):
-    logger.info("test_wrong_primary_name")
-    new_config_data = copy(corsika_config_data)
-    new_config_data["wrong_par"] = 20 * u.m
     with pytest.raises(InvalidCorsikaInputError):
-        corsika_test_config = CorsikaConfig(
-            array_model=array_model_north,
-            label="test-corsika-config",
-            corsika_config_data=new_config_data,
-        )
-        corsika_test_config.print_user_parameters()
+        cc._convert_primary_input_and_store_primary_name("banana")
 
 
-def test_units_of_config_data(corsika_config, corsika_config_data, array_model_north):
-    logger.info("test_units_of_config_data")
-    new_config_data = copy(corsika_config_data)
-    new_config_data["zenith"] = 20 * u.m
-    with pytest.raises(InvalidCorsikaInputError):
-        corsika_test_config = CorsikaConfig(
-            array_model=array_model_north,
-            label="test-corsika-config",
-            corsika_config_data=new_config_data,
-        )
-        corsika_test_config.print_user_parameters()
+def test_get_config_parameter(io_handler, corsika_config_data, caplog):
+    cc = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=corsika_config_data,
+    )
+    assert isinstance(cc.get_config_parameter("NSHOW"), int)
+    assert isinstance(cc.get_config_parameter("THETAP"), list)
+    with pytest.raises(KeyError):
+        cc.get_config_parameter("not_really_a_parameter")
+    assert "Parameter not_really_a_parameter" in caplog.text
 
 
-def test_len_of_config_data(corsika_config, corsika_config_data, array_model_north):
-    logger.info("test_len_of_config_data")
-    new_config_data = copy(corsika_config_data)
-    new_config_data["erange"] = [20 * u.TeV]
-    with pytest.raises(InvalidCorsikaInputError):
-        corsika_test_config = CorsikaConfig(
-            array_model=array_model_north,
-            label="test-corsika-config",
-            corsika_config_data=new_config_data,
-        )
-        corsika_test_config.print_user_parameters()
+def test_print_config_parameter(corsika_config, capsys):
+    logger.info("test_print_config_parameter")
+    corsika_config.print_config_parameter()
+    assert "NSHOW" in capsys.readouterr().out
 
 
-def test_wrong_primary_name(corsika_config, corsika_config_data, array_model_north):
-    logger.info("test_wrong_primary_name")
-    new_config_data = copy(corsika_config_data)
-    new_config_data["primary"] = "rock"
-    with pytest.raises(InvalidCorsikaInputError):
-        corsika_test_config = CorsikaConfig(
-            array_model=array_model_north,
-            label="test-corsika-config",
-            corsika_config_data=new_config_data,
-        )
-        corsika_test_config.print_user_parameters()
-
-
-def test_missing_input(corsika_config, corsika_config_data, array_model_north):
-    logger.info("test_missing_input")
-    new_config_data = copy(corsika_config_data)
-    new_config_data.pop("primary")
-    with pytest.raises(MissingRequiredInputInCorsikaConfigDataError):
-        corsika_test_config = CorsikaConfig(
-            array_model=array_model_north,
-            label="test-corsika-config",
-            corsika_config_data=new_config_data,
+def test_get_text_single_line(io_handler):
+    cc = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=None,
+    )
+    assert cc._get_text_single_line({"EVTNR": [1], "RUNNR": [10]}) == "EVTNR 1 \nRUNNR 10 \n"
+    assert (
+        cc._get_text_single_line(
+            {"SPLIT_AUTO": ["15M"], "IO_BUFFER": ["800MB"], "MAX_BUNCHES": ["1000000"]}, "IACT "
         )
-        corsika_test_config.print_user_parameters()
+        == "IACT SPLIT_AUTO 15M \nIACT IO_BUFFER 800MB \nIACT MAX_BUNCHES 1000000 \n"
+    )
 
 
-def test_set_user_parameters(corsika_config_data, corsika_config):
-    logger.info("test_set_user_parameters")
-    new_config_data = copy(corsika_config_data)
-    new_config_data["zenith"] = 0 * u.deg
-    new_corsika_config = copy(corsika_config)
-    new_corsika_config.set_user_parameters(new_config_data)
+def test_generate_corsika_input_file(corsika_config):
+    logger.info("test_generate_corsika_input_file")
+    input_file = corsika_config.generate_corsika_input_file()
+    assert input_file.exists()
+    with open(input_file) as f:
+        assert "TELFIL |" not in f.read()
 
-    assert new_corsika_config.get_user_parameter("thetap") == [0, 0]
+    assert corsika_config._is_file_updated
+    assert input_file == corsika_config.generate_corsika_input_file()
 
 
-def test_config_data_from_yaml_file(io_handler, array_model_south):
-    logger.info("test_config_data_from_yaml_file")
-    cc = CorsikaConfig(
-        array_model=array_model_south,
-        label="test-corsika-config",
-        corsika_config_file="tests/resources/corsikaConfigTest.yml",
-    )
-    cc.print_user_parameters()
-
-    test_dict = {
-        "RUNNR": [10],
-        "NSHOW": [100],
-        "PRMPAR": [14],
-        "ERANGE": [0.01, 10.0],
-        "ESLOPE": [-2],
-        "THETAP": [20.0, 20.0],
-        "AZM": [0.0, 0.0],
-        "CSCAT": [10, 150000.0, 0],
-        "VIEWCONE": [0.0, 5.0],
-        "EVTNR": [1],
-        "PHIP": [180.0, 180.0],
-    }
-    assert test_dict == cc._user_parameters
-
-
-def test_get_file_name(corsika_config, io_handler):
+def test_generate_corsika_input_file_multipipe(corsika_config):
+    logger.info("test_generate_corsika_input_file")
+    input_file = corsika_config.generate_corsika_input_file(use_multipipe=True)
+    assert input_file.exists()
+    with open(input_file) as f:
+        assert "TELFIL |" in f.read()
+
+
+def test_get_corsika_config_file_name(corsika_config, io_handler):
     file_name = "proton_South_test_layout_za020-azm000deg_cone0-5_test-corsika-config"
 
     assert (
-        corsika_config.get_file_name("config_tmp", run_number=1)
+        corsika_config.get_corsika_config_file_name("config_tmp", run_number=1)
         == f"corsika_config_run000001_{file_name}.txt"
     )
     with pytest.raises(ValueError):
         assert (
-            corsika_config.get_file_name("config_tmp")
+            corsika_config.get_corsika_config_file_name("config_tmp")
             == f"corsika_config_run000001_{file_name}.txt"
         )
 
-    assert corsika_config.get_file_name("config") == f"corsika_config_{file_name}.input"
+    assert (
+        corsika_config.get_corsika_config_file_name("config") == f"corsika_config_{file_name}.input"
+    )
     # The test below includes the placeholder XXXXXX for the run number because
     # that is the way we get the run number later in the CORSIKA input file with zero padding.
-    assert corsika_config.get_file_name("output_generic") == (
-        "corsika_runXXXXXX_proton_za020deg_azm000deg_South_test_layout_test-corsika-config.zst"
+    assert corsika_config.get_corsika_config_file_name("output_generic") == (
+        "runXXXXXX_proton_South_test_layout_za020-azm000deg_cone0-5_test"
+        "-corsika-config_South_test_layout_test-corsika-config.zst"
+    )
+    assert (
+        corsika_config.get_corsika_config_file_name("multipipe")
+        == "multi_cta-South-test_layout.cfg"
     )
-    assert corsika_config.get_file_name("multipipe") == "multi_cta-South-test_layout.cfg"
     with pytest.raises(ValueError):
-        corsika_config.get_file_name("foobar")
-
-
-def test_convert_to_quantities(corsika_config):
-
-    assert corsika_config._convert_to_quantities("10 m") == [10 * u.m]
-    assert corsika_config._convert_to_quantities("simple_string") == ["simple_string"]
-    assert corsika_config._convert_to_quantities({"value": 10, "unit": "m"}) == [10 * u.m]
-    assert corsika_config._convert_to_quantities({"not_value": 10, "not_unit": "m"}) == [
-        {"not_value": 10, "not_unit": "m"}
-    ]
-    assert corsika_config._convert_to_quantities(["10 m", "20 m", "simple_string"]) == [
-        10 * u.m,
-        20 * u.m,
-        "simple_string",
-    ]
-    assert corsika_config._convert_to_quantities(
-        [{"value": 10, "unit": "m"}, "20 m", "simple_string"]
-    ) == [10 * u.m, 20 * u.m, "simple_string"]
+        corsika_config.get_corsika_config_file_name("foobar")
+
+
+def test_set_output_file_and_directory(corsika_config):
+    cc = corsika_config
+    output_file = cc.set_output_file_and_directory()
+    assert str(output_file) == (
+        "runXXXXXX_proton_South_test_layout_za020-azm000deg_cone0-5_test"
+        "-corsika-config_South_test_layout_test-corsika-config.zst"
+    )
+    assert isinstance(cc.config_file_path, pathlib.Path)
+
+
+def test_write_seeds(io_handler):
+    mock_file = Mock()
+    corsika_config = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=None,
+    )
+    corsika_config.run_number = 10
+    corsika_config.config = {"PRMPAR": [14]}
+    with patch("io.open", return_value=mock_file):
+        corsika_config._write_seeds(mock_file)
+    assert mock_file.write.call_count == 4
+
+    expected_calls = [_call.args[0] for _call in mock_file.write.call_args_list]
+    for _call in expected_calls:
+        assert _call.startswith("SEED ")
+        assert _call.endswith(" 0 0\n")
+
+
+def test_get_corsika_telescope_list(corsika_config):
+    cc = corsika_config
+    telescope_list_str = cc.get_corsika_telescope_list()
+    assert telescope_list_str.count("TELESCOPE") > 0
+    assert telescope_list_str.count("LSTS") > 0
+
+
+def test_run_number(io_handler):
+    empty_corsika_config = CorsikaConfig(
+        array_model=None,
+        label="test-corsika-config",
+        args_dict=None,
+    )
+    assert empty_corsika_config.run_number is None
+    empty_corsika_config.run_number = 25
+    assert empty_corsika_config.run_number == 25
+
+
+def test_validate_run_number(corsika_config):
+    # default value from corsika_config
+    assert corsika_config.validate_run_number(None) == 1
+    assert corsika_config.validate_run_number(1)
+    assert corsika_config.validate_run_number(123456)
+    with pytest.raises(ValueError, match=r"^could not convert string to float"):
+        corsika_config.validate_run_number("test")
+    invalid_run_number = r"^Invalid type of run number"
+    with pytest.raises(ValueError, match=invalid_run_number):
+        corsika_config.validate_run_number(1.5)
+    with pytest.raises(ValueError, match=invalid_run_number):
+        corsika_config.validate_run_number(-1)
+    with pytest.raises(ValueError, match=invalid_run_number):
+        corsika_config.validate_run_number(123456789)
diff --git a/tests/unit_tests/corsika/test_corsika_default_config.py b/tests/unit_tests/corsika/test_corsika_default_config.py
deleted file mode 100644
index 4ebeef792..000000000
--- a/tests/unit_tests/corsika/test_corsika_default_config.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import astropy.units as u
-import pytest
-
-from simtools.corsika.corsika_default_config import CorsikaDefaultConfig
-
-
-def compare_lists_ignoring_units(list1, list2):
-    return all(list1[i].value == pytest.approx(list2[i]) for i in range(len(list1)))
-
-
-def test_init():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-    assert config.primary == "gamma"
-    assert config.zenith_angle.value == pytest.approx(20.0)
-
-
-def test_invalid_init(caplog):
-    with pytest.raises(ValueError):
-        CorsikaDefaultConfig(primary="invalid_particle", zenith_angle=20.0 * u.deg)
-        assert "Invalid primary particle: invalid_particle" in caplog.text
-
-
-def test_primary_setter():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-    config.primary = "proton"
-    assert config.primary == "proton"
-
-
-def test_primary_setter_invalid(caplog):
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-    with pytest.raises(ValueError):
-        config.primary = "invalid_particle"
-        assert "Invalid primary particle: invalid_particle" in caplog.text
-
-
-def test_zenith_angle_setter():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-    config.zenith_angle = 30.0 * u.deg
-    assert config.zenith_angle.value == pytest.approx(30.0)
-
-
-def test_zenith_angle_setter_invalid(caplog):
-    with pytest.raises(ValueError):
-        config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-        config.zenith_angle = 5.0 * u.deg
-        assert "outside of the allowed interval" in caplog.text
-
-
-def test_energy_range_for_primary():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=40.0 * u.deg)
-    energy_range = config.energy_range_for_primary()
-    expected_energies = [6.0, 660.0]
-    compare_lists_ignoring_units(energy_range, expected_energies)
-
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=52.0 * u.deg)
-    energy_range = config.energy_range_for_primary()
-    expected_energies = [9.24, 858.0]
-    compare_lists_ignoring_units(energy_range, expected_energies)
-
-
-def test_number_of_showers_for_primary():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=40.0 * u.deg)
-    number_of_showers = config.number_of_showers_for_primary()
-    assert number_of_showers == 5000
-
-
-def test_view_cone_for_primary():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-    view_cone = config.view_cone_for_primary()
-    expected_view_cone = [0.0, 0.0]
-    compare_lists_ignoring_units(view_cone, expected_view_cone)
-
-    config = CorsikaDefaultConfig(primary="gamma_diffuse", zenith_angle=20.0 * u.deg)
-    view_cone = config.view_cone_for_primary()
-    expected_view_cone = [0.0, 10.0]
-    compare_lists_ignoring_units(view_cone, expected_view_cone)
-
-    config = CorsikaDefaultConfig(primary="proton", zenith_angle=20.0 * u.deg)
-    view_cone = config.view_cone_for_primary()
-    expected_view_cone = [0.0, 10.0]
-    compare_lists_ignoring_units(view_cone, expected_view_cone)
-
-
-def test_interpolate_to_zenith_angle():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-
-    # Test case 1: Interpolate energy range
-    zenith_angles_to_interpolate = [30.0, 40.0, 50.0]
-    values_to_interpolate = [6.0, 9.24, 12.5]
-    interpolated_value = config.interpolate_to_zenith_angle(
-        35.0 * u.deg, zenith_angles_to_interpolate, values_to_interpolate
-    )
-    assert interpolated_value == pytest.approx(7.6175)
-
-    # Test case 2: Interpolate number of showers
-    zenith_angles_to_interpolate = [30.0, 40.0, 50.0]
-    values_to_interpolate = [5000, 6000, 7000]
-    interpolated_value = config.interpolate_to_zenith_angle(
-        45.0 * u.deg, zenith_angles_to_interpolate, values_to_interpolate
-    )
-    assert interpolated_value == pytest.approx(6500)
-
-
-def test_energy_slope_getter():
-    config = CorsikaDefaultConfig(primary="gamma", zenith_angle=20.0 * u.deg)
-    assert config.energy_slope == pytest.approx(-2.0)
diff --git a/tests/unit_tests/corsika/test_corsika_runner.py b/tests/unit_tests/corsika/test_corsika_runner.py
deleted file mode 100644
index d3c8ff3ef..000000000
--- a/tests/unit_tests/corsika/test_corsika_runner.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/python3
-
-import logging
-import shutil
-
-import astropy.units as u
-import pytest
-
-from simtools.corsika.corsika_runner import CorsikaRunner
-
-logger = logging.getLogger()
-logger.setLevel(logging.DEBUG)
-
-
-@pytest.fixture()
-def corsika_config_data(tmp_test_directory):
-    return {
-        "data_directory": f"{tmp_test_directory!s}/test-output",
-        "nshow": 10,
-        "primary": "gamma",
-        "erange": [100 * u.GeV, 1 * u.TeV],
-        "eslope": -2,
-        "zenith": 20 * u.deg,
-        "azimuth": 0 * u.deg,
-        "viewcone": 0 * u.deg,
-        "cscat": [10, 1500 * u.m, 0],
-    }
-
-
-@pytest.fixture()
-def corsika_runner(corsika_config_data, io_handler, simtel_path, array_model_south):
-    return CorsikaRunner(
-        simtel_path=simtel_path,
-        label="test-corsika-runner",
-        corsika_config_data=corsika_config_data,
-        array_model=array_model_south,
-    )
-
-
-@pytest.fixture()
-def corsika_file(io_handler):
-    return io_handler.get_input_data_file(
-        file_name="run1_proton_za20deg_azm0deg_North_1LST_test-lst-array.corsika.zst", test=True
-    )
-
-
-def test_prepare_run_script(corsika_runner):
-    # No run number is given
-
-    script = corsika_runner.prepare_run_script()
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "/usr/bin/env bash" in script_content
-        assert "corsika_autoinputs" in script_content
-        assert "sim_telarray/bin/pfp" in script_content
-
-    # Run number is given
-    run_number = 3
-    script = corsika_runner.prepare_run_script(run_number=run_number)
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "/usr/bin/env bash" in script_content
-        assert "corsika_autoinputs" in script_content
-        assert "sim_telarray/bin/pfp" in script_content
-        assert "-R 3" in script_content
-
-
-def test_prepare_run_script_with_invalid_run(corsika_runner):
-    for run in [-2, "test"]:
-        with pytest.raises(ValueError):
-            _ = corsika_runner.prepare_run_script(run_number=run)
-
-
-def test_prepare_run_script_with_extra(corsika_runner, file_has_text):
-    extra = ["testing", "testing-extra-2"]
-    script = corsika_runner.prepare_run_script(run_number=3, extra_commands=extra)
-
-    assert file_has_text(script, "testing-extra-2")
-    with open(script) as f:
-        script_content = f.read()
-        assert "/usr/bin/env bash" in script_content
-        assert "corsika_autoinputs" in script_content
-        assert "sim_telarray/bin/pfp" in script_content
-
-
-def test_prepare_run_script_without_pfp(corsika_runner):
-    script = corsika_runner.prepare_run_script(use_pfp=False)
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "/usr/bin/env bash" in script_content
-        assert "corsika_autoinputs" in script_content
-        assert "sim_telarray/bin/pfp" not in script_content
-
-
-def test_get_info_for_file_name(corsika_runner):
-    info_for_file_name = corsika_runner.get_info_for_file_name(run_number=1)
-    assert info_for_file_name["run"] == 1
-    assert info_for_file_name["primary"] == "gamma"
-    assert info_for_file_name["array_name"] == "test_layout"
-    assert info_for_file_name["site"] == "South"
-    assert info_for_file_name["label"] == "test-corsika-runner"
-
-
-def test_get_file_name(corsika_runner, io_handler):
-    info_for_file_name = corsika_runner.get_info_for_file_name(run_number=1)
-    file_name = "corsika_run000001_gamma_South_test_layout_test-corsika-runner"
-
-    assert corsika_runner.get_file_name(
-        "corsika_autoinputs_log", **info_for_file_name
-    ) == corsika_runner._corsika_log_dir.joinpath(f"log_{file_name}.log.gz")
-
-    assert corsika_runner.get_file_name(
-        "corsika_log", **info_for_file_name
-    ) == corsika_runner._corsika_data_dir.joinpath(corsika_runner._get_run_directory(1)).joinpath(
-        "run1.log"
-    )
-
-    script_file_dir = io_handler.get_output_directory("test-corsika-runner", "corsika").joinpath(
-        "scripts"
-    )
-    assert corsika_runner.get_file_name("script", **info_for_file_name) == script_file_dir.joinpath(
-        f"{file_name}.sh"
-    )
-
-    file_name_for_output = (
-        "corsika_run000001_gamma_za020deg_azm000deg_South_test_layout_test-corsika-runner.zst"
-    )
-    assert corsika_runner.get_file_name(
-        "output", **info_for_file_name
-    ) == corsika_runner._corsika_data_dir.joinpath(corsika_runner._get_run_directory(1)).joinpath(
-        file_name_for_output
-    )
-
-    sub_log_file_dir = io_handler.get_output_directory("test-corsika-runner", "corsika").joinpath(
-        "logs"
-    )
-    assert corsika_runner.get_file_name(
-        "sub_log", **info_for_file_name, mode="out"
-    ) == sub_log_file_dir.joinpath(f"log_sub_{file_name}.out")
-    with pytest.raises(ValueError):
-        corsika_runner.get_file_name("foobar", **info_for_file_name, mode="out")
-    assert corsika_runner.get_file_name(
-        "sub_log", **info_for_file_name, mode=""
-    ) == sub_log_file_dir.joinpath(f"log_sub_{file_name}.log")
-
-
-def test_has_file(corsika_runner, corsika_file):
-    # Copying the corsika file to the expected location and
-    # changing its name for the sake of this test.
-    # This should not affect the efficiency of this test.
-    output_directory = corsika_runner._corsika_data_dir.joinpath(
-        corsika_runner._get_run_directory(1)
-    )
-    output_directory.mkdir(parents=True, exist_ok=True)
-    shutil.copy(
-        corsika_file,
-        output_directory.joinpath(
-            "corsika_run000001_gamma_za020deg_azm000deg_South_test_layout_test-corsika-runner.zst"
-        ),
-    )
-    assert corsika_runner.has_file(file_type="output", run_number=1)
-    assert not corsika_runner.has_file(file_type="corsika_autoinputs_log", run_number=1234)
diff --git a/tests/unit_tests/corsika_simtel/test_corsika_simtel_runner.py b/tests/unit_tests/corsika_simtel/test_corsika_simtel_runner.py
deleted file mode 100644
index 50a1a56f7..000000000
--- a/tests/unit_tests/corsika_simtel/test_corsika_simtel_runner.py
+++ /dev/null
@@ -1,175 +0,0 @@
-#!/usr/bin/python3
-
-import logging
-from pathlib import Path
-
-import pytest
-
-from simtools.corsika_simtel.corsika_simtel_runner import CorsikaSimtelRunner
-from simtools.model.array_model import ArrayModel
-
-logger = logging.getLogger()
-logger.setLevel(logging.DEBUG)
-
-
-@pytest.fixture()
-def common_args(simtel_path):
-    return {
-        "label": "test-corsika-simtel-runner",
-        "simtel_path": simtel_path,
-    }
-
-
-@pytest.fixture()
-def array_model(simulator_config_data_north, io_handler, db_config, common_args, model_version):
-    return ArrayModel(
-        label=common_args["label"],
-        site=simulator_config_data_north["common"]["site"],
-        layout_name=simulator_config_data_north["common"]["layout_name"],
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
-
-
-@pytest.fixture()
-def corsika_args(
-    array_model,
-    shower_config_data_north,
-):
-    # Remove the keys which are not necessary for general CORSIKA configuration
-    for key_to_pop in ["site", "run_list", "run_range", "layout_name"]:
-        shower_config_data_north.pop(key_to_pop, None)
-    return {
-        "array_model": array_model,
-        "corsika_config_data": shower_config_data_north,
-    }
-
-
-@pytest.fixture()
-def simtel_config_data(tmp_test_directory, simulator_config_data_north):
-    return {
-        "simtel_data_directory": str(tmp_test_directory) + "/test-output",
-        "primary": simulator_config_data_north["common"]["primary"],
-        "zenith_angle": simulator_config_data_north["common"]["zenith"],
-        "azimuth_angle": simulator_config_data_north["common"]["azimuth"],
-    }
-
-
-@pytest.fixture()
-def simtel_args(array_model, simtel_config_data):
-    return {"array_model": array_model, "config_data": simtel_config_data}
-
-
-@pytest.fixture()
-def corsika_simtel_runner(common_args, corsika_args, simtel_args):
-    return CorsikaSimtelRunner(
-        common_args=common_args, corsika_args=corsika_args, simtel_args=simtel_args
-    )
-
-
-def test_prepare_run_script(corsika_simtel_runner):
-    # No run number is given
-
-    script = corsika_simtel_runner.prepare_run_script()
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "/usr/bin/env bash" in script_content
-        assert "corsika_autoinputs" in script_content
-        assert "sim_telarray/bin/pfp" not in script_content
-
-    # Run number is given
-    run_number = 3
-    script = corsika_simtel_runner.prepare_run_script(run_number=run_number)
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "/usr/bin/env bash" in script_content
-        assert "corsika_autoinputs" in script_content
-        assert "sim_telarray/bin/pfp" not in script_content
-        assert "-R 3" in script_content
-
-
-def test_prepare_run_script_with_invalid_run(corsika_simtel_runner):
-    for run in [-2, "test"]:
-        with pytest.raises(ValueError):
-            _ = corsika_simtel_runner.prepare_run_script(run_number=run)
-
-
-def test_export_multipipe_script(corsika_simtel_runner):
-    corsika_simtel_runner.export_multipipe_script()
-    script = Path(corsika_simtel_runner.corsika_config.config_file_path.parent).joinpath(
-        corsika_simtel_runner.corsika_config.get_file_name("multipipe")
-    )
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "bin/sim_telarray" in script_content
-        assert "-C telescope_theta=20" in script_content
-        assert "-C telescope_phi=0" in script_content
-        assert "-C show=all" in script_content
-
-
-def test_export_multipipe_executable(corsika_simtel_runner):
-    corsika_simtel_runner.export_multipipe_script()
-    multipipe_file = Path(corsika_simtel_runner.corsika_config.config_file_path.parent).joinpath(
-        corsika_simtel_runner.corsika_config.get_file_name("multipipe")
-    )
-    corsika_simtel_runner._export_multipipe_executable(multipipe_file)
-    script = Path(corsika_simtel_runner.corsika_config.config_file_path.parent).joinpath(
-        "run_cta_multipipe"
-    )
-
-    assert script.exists()
-    with open(script) as f:
-        script_content = f.read()
-        assert "bin/multipipe_corsika" in script_content
-        assert f"-c {multipipe_file}" in script_content
-        assert "'Fan-out failed'" in script_content
-
-
-def test_make_run_command(corsika_simtel_runner):
-    command = corsika_simtel_runner._make_run_command(input_file="-", run_number=1)
-    assert "bin/sim_telarray" in command
-    assert "-C telescope_theta=20" in command
-    assert "-C telescope_phi=0" in command
-    assert "-C show=all" in command
-    assert "run000001_gamma_za020deg_azm000deg_North_test_layout_test" in command
-
-
-def test_make_run_command_divergent(corsika_simtel_runner):
-    corsika_simtel_runner.label = "test-corsika-simtel-runner-divergent-pointing"
-    command = corsika_simtel_runner._make_run_command(input_file="-", run_number=1)
-    assert "bin/sim_telarray" in command
-    assert "-W telescope_theta=20" in command
-    assert "-W telescope_phi=0" in command
-    assert "-C show=all" in command
-    assert "run000001_gamma_za020deg_azm000deg_North_test_layout_test" in command
-
-
-def test_get_info_for_file_name(corsika_simtel_runner):
-    info_for_file_name = corsika_simtel_runner.get_info_for_file_name(run_number=1)
-    assert info_for_file_name["run"] == 1
-    assert info_for_file_name["primary"] == "gamma"
-    assert info_for_file_name["array_name"] == "test_layout"
-    assert info_for_file_name["site"] == "North"
-    assert info_for_file_name["label"] == "test-corsika-simtel-runner"
-
-
-def test_get_file_name(corsika_simtel_runner, io_handler):
-    info_for_file_name = corsika_simtel_runner.get_info_for_file_name(run_number=1)
-
-    # Test one case of a CORSIKA file. Other cases are tested in the corsika_runner tests
-    file_name = "corsika_run000001_gamma_North_test_layout_test-corsika-simtel-runner"
-    assert corsika_simtel_runner.get_file_name(
-        "corsika_autoinputs_log", **info_for_file_name
-    ) == corsika_simtel_runner._corsika_log_dir.joinpath(f"log_{file_name}.log.gz")
-
-    # Test the histogram case which calls the simulator_array internally
-    file_name = "run000001_gamma_za020deg_azm000deg_North_test_layout_test-corsika-simtel-runner"
-    assert corsika_simtel_runner.get_file_name(
-        "histogram", **info_for_file_name
-    ) == corsika_simtel_runner._simtel_log_dir.joinpath(f"{file_name}.hdata.zst")
diff --git a/tests/unit_tests/model/test_array_model.py b/tests/unit_tests/model/test_array_model.py
index 4006380d1..b58335f0b 100644
--- a/tests/unit_tests/model/test_array_model.py
+++ b/tests/unit_tests/model/test_array_model.py
@@ -7,7 +7,7 @@
 from astropy import units as u
 from astropy.table import QTable
 
-from simtools.model.array_model import ArrayModel, InvalidArrayConfigDataError
+from simtools.model.array_model import ArrayModel
 
 logger = logging.getLogger()
 logger.setLevel(logging.DEBUG)
@@ -57,78 +57,6 @@ def test_site(array_model):
     assert am.site == "North"
 
 
-def test_get_single_telescope_info_from_array_config(db_config, model_version, io_handler):
-    parameters_to_change = {
-        "MSTN-05": {  # change MST pulse shape for testing to LST pulse shape
-            "name": "MSTN-05",
-            "fadc_pulse_shape": "LST_pulse_shape_7dynode_high_intensity_pix1s.dat",
-        },
-    }
-    am = ArrayModel(
-        label="test",
-        site="North",
-        layout_name="test_layout",
-        parameters_to_change=parameters_to_change,
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
-
-    assert am._get_single_telescope_info_from_array_config("LSTN-01", parameters_to_change) == {}
-    assert am._get_single_telescope_info_from_array_config("MSTN-05", parameters_to_change) == {
-        "fadc_pulse_shape": "LST_pulse_shape_7dynode_high_intensity_pix1s.dat"
-    }
-
-    parameters_missing_name = {
-        "MSTN-05": {  # change MST pulse shape for testing to LST pulse shape
-            "fadc_pulse_shape": "LST_pulse_shape_7dynode_high_intensity_pix1s.dat",
-        },
-    }
-    with pytest.raises(
-        InvalidArrayConfigDataError, match="ArrayConfig has no name for a telescope"
-    ):
-        ArrayModel(
-            label="test",
-            site="North",
-            layout_name="test_layout",
-            parameters_to_change=parameters_missing_name,
-            mongo_db_config=db_config,
-            model_version=model_version,
-        )
-
-    parameters_with_string = {
-        "MSTN-05": "a string",
-    }
-    am_with_string = ArrayModel(
-        label="test",
-        site="North",
-        layout_name="test_layout",
-        parameters_to_change=parameters_with_string,
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
-    assert (
-        am_with_string._get_single_telescope_info_from_array_config(
-            "MSTN-05", parameters_with_string
-        )
-        == {}
-    )
-
-    invalid_parameters = {
-        "MSTN-05": 5.0,
-    }
-    with pytest.raises(
-        InvalidArrayConfigDataError, match="ArrayConfig has wrong input for a telescope"
-    ):
-        ArrayModel(
-            label="test",
-            site="North",
-            layout_name="test_layout",
-            parameters_to_change=invalid_parameters,
-            mongo_db_config=db_config,
-            model_version=model_version,
-        )
-
-
 def test_exporting_config_files(db_config, io_handler, model_version):
     am = ArrayModel(
         label="test",
@@ -195,11 +123,14 @@ def test_get_telescope_position_parameter(array_model, io_handler):
     }
 
 
-def test_set_config_file_directory(array_model, io_handler):
+def test_get_config_file(array_model, io_handler):
+    am = array_model
+    assert am.get_config_file().name == "CTA-test_layout-North-2024-02-01_test.cfg"
+
+
+def test_get_config_directory(array_model, io_handler):
     am = array_model
-    _config_dir_1 = am.io_handler.get_output_directory(am.label, "model")
-    am._set_config_file_directory()
-    assert _config_dir_1.is_dir()
+    assert am.get_config_directory().is_dir()
 
 
 def test_export_array_elements_as_table(array_model, io_handler):
diff --git a/tests/unit_tests/runners/test_corsika_runner.py b/tests/unit_tests/runners/test_corsika_runner.py
new file mode 100644
index 000000000..09dae73de
--- /dev/null
+++ b/tests/unit_tests/runners/test_corsika_runner.py
@@ -0,0 +1,122 @@
+#!/usr/bin/python3
+
+import logging
+import pathlib
+import re
+
+import pytest
+
+logger = logging.getLogger()
+logger.setLevel(logging.DEBUG)
+
+
+@pytest.fixture()
+def bin_bash():
+    """Path to bash."""
+    return "/usr/bin/env bash"
+
+
+@pytest.fixture()
+def pfp_command():
+    """Basic pfp command."""
+    return "sim_telarray/bin/pfp"
+
+
+def test_corsika_runner(corsika_runner):
+    cr = corsika_runner
+    assert "corsika_simtel" not in str(cr._directory["output"])
+    assert "corsika" in str(cr._directory["output"])
+    assert isinstance(cr._directory["data"], pathlib.Path)
+
+
+def test_prepare_run_script(corsika_runner, bin_bash, pfp_command):
+    # No run number is given
+    script = corsika_runner.prepare_run_script()
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert bin_bash in script_content
+        assert "corsika_autoinputs" in script_content
+        assert pfp_command in script_content
+
+    # Run number is given
+    script = corsika_runner.prepare_run_script(run_number=3)
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert bin_bash in script_content
+        assert "corsika_autoinputs" in script_content
+        assert pfp_command in script_content
+        assert "-R 3" in script_content
+
+
+def test_prepare_run_script_with_input_file(corsika_runner, caplog):
+    with caplog.at_level("WARNING"):
+        corsika_runner.prepare_run_script(input_file="test")
+    assert any(
+        "input_file parameter is not used in CorsikaRunner.prepare_run_script" in message
+        for message in caplog.messages
+    )
+
+
+def test_prepare_run_script_with_invalid_run(corsika_runner):
+    for run in [-2, "test"]:
+        with pytest.raises(ValueError):
+            _ = corsika_runner.prepare_run_script(run_number=run)
+
+
+def test_prepare_run_script_with_extra(corsika_runner, file_has_text, bin_bash, pfp_command):
+    extra = ["testing", "testing-extra-2"]
+    script = corsika_runner.prepare_run_script(run_number=3, extra_commands=extra)
+
+    assert file_has_text(script, "testing-extra-2")
+    with open(script) as f:
+        script_content = f.read()
+        assert bin_bash in script_content
+        assert "corsika_autoinputs" in script_content
+        assert pfp_command in script_content
+
+
+def test_prepare_run_script_without_pfp(corsika_runner, bin_bash, pfp_command):
+    script = corsika_runner.prepare_run_script(use_pfp=False)
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert bin_bash in script_content
+        assert "corsika_autoinputs" in script_content
+        assert pfp_command not in script_content
+
+
+def test_get_pfp_command(corsika_runner, pfp_command):
+    command = corsika_runner._get_pfp_command("input_tmp_file", "corsika_input_file")
+    assert pfp_command in command
+    assert "> input_tmp_file" in command
+
+
+def test_get_autoinputs_command(corsika_runner):
+    autoinputs_command = corsika_runner._get_autoinputs_command(
+        run_number=3, input_tmp_file="tmp_file"
+    )
+    assert "corsika_autoinputs" in autoinputs_command
+    assert "-R 3" in autoinputs_command
+    assert "--keep-seeds" not in autoinputs_command
+    corsika_runner._keep_seeds = True
+    autoinputs_command_with_seeds = corsika_runner._get_autoinputs_command(
+        run_number=3, input_tmp_file="tmp_file"
+    )
+    assert "--keep-seeds" in autoinputs_command_with_seeds
+
+
+def test_get_resources(corsika_runner):
+    with pytest.raises(FileNotFoundError):
+        corsika_runner.get_resources()
+
+
+def test_get_file_name(corsika_runner):
+    with pytest.raises(
+        ValueError, match=re.escape("simulation_software (test) is not supported in CorsikaRunner")
+    ):
+        corsika_runner.get_file_name(simulation_software="test")
diff --git a/tests/unit_tests/runners/test_corsika_simtel_runner.py b/tests/unit_tests/runners/test_corsika_simtel_runner.py
new file mode 100644
index 000000000..a55669c1b
--- /dev/null
+++ b/tests/unit_tests/runners/test_corsika_simtel_runner.py
@@ -0,0 +1,177 @@
+#!/usr/bin/python3
+
+import copy
+import logging
+from pathlib import Path
+
+import pytest
+
+from simtools.runners.corsika_simtel_runner import (
+    CorsikaRunner,
+    CorsikaSimtelRunner,
+    SimulatorArray,
+)
+
+logger = logging.getLogger()
+logger.setLevel(logging.DEBUG)
+
+
+@pytest.fixture()
+def simtel_command():
+    """Basic simtel command."""
+    return "bin/sim_telarray"
+
+
+@pytest.fixture()
+def show_all():
+    """Simtel show all options."""
+    return "-C show=all"
+
+
+@pytest.fixture()
+def simulation_file():
+    """Base name for simulation test file."""
+    return "run000001_proton_za20deg_azm000deg_South_test_layout_test-corsika-simtel-runner"
+
+
+@pytest.fixture()
+def corsika_simtel_runner(io_handler, corsika_config, simtel_path):
+    """CorsikaSimtelRunner object."""
+    return CorsikaSimtelRunner(
+        corsika_config=corsika_config,
+        simtel_path=simtel_path,
+        label="test-corsika-simtel-runner",
+        use_multipipe=True,
+    )
+
+
+def test_corsika_simtel_runner(corsika_simtel_runner):
+
+    assert isinstance(corsika_simtel_runner.corsika_runner, CorsikaRunner)
+    assert isinstance(corsika_simtel_runner.simulator_array, SimulatorArray)
+
+
+def test_prepare_run_script(corsika_simtel_runner):
+    # No run number is given
+
+    script = corsika_simtel_runner.prepare_run_script()
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert "/usr/bin/env bash" in script_content
+        assert "corsika_autoinputs" in script_content
+        assert "sim_telarray/bin/pfp" not in script_content
+
+    # Run number is given
+    run_number = 3
+    script = corsika_simtel_runner.prepare_run_script(run_number=run_number)
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert "/usr/bin/env bash" in script_content
+        assert "corsika_autoinputs" in script_content
+        assert "sim_telarray/bin/pfp" not in script_content
+        assert "-R 3" in script_content
+
+
+def test_prepare_run_script_with_invalid_run(corsika_simtel_runner):
+    for run_number in [-2, "test"]:
+        with pytest.raises(ValueError):
+            _ = corsika_simtel_runner.prepare_run_script(run_number=run_number)
+
+
+def test_export_multipipe_script(corsika_simtel_runner, simtel_command, show_all):
+    corsika_simtel_runner._export_multipipe_script(run_number=1)
+    script = Path(corsika_simtel_runner.corsika_config.config_file_path.parent).joinpath(
+        corsika_simtel_runner.corsika_config.get_corsika_config_file_name("multipipe")
+    )
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert simtel_command in script_content
+        assert "-C telescope_theta=20" in script_content
+        assert "-C telescope_phi=0" in script_content
+        assert show_all in script_content
+
+
+def test_write_multipipe_script(corsika_simtel_runner):
+    corsika_simtel_runner._export_multipipe_script(run_number=1)
+    multipipe_file = Path(corsika_simtel_runner.corsika_config.config_file_path.parent).joinpath(
+        corsika_simtel_runner.corsika_config.get_corsika_config_file_name("multipipe")
+    )
+    corsika_simtel_runner._write_multipipe_script(multipipe_file)
+    script = Path(corsika_simtel_runner.corsika_config.config_file_path.parent).joinpath(
+        "run_cta_multipipe"
+    )
+
+    assert script.exists()
+    with open(script) as f:
+        script_content = f.read()
+        assert "bin/multipipe_corsika" in script_content
+        assert f"-c {multipipe_file}" in script_content
+        assert "'Fan-out failed'" in script_content
+
+
+def test_make_run_command(corsika_simtel_runner, simtel_command, show_all):
+    command = corsika_simtel_runner._make_run_command(input_file="-", run_number=1)
+    assert simtel_command in command
+    assert "-C telescope_theta=20" in command
+    assert "-C telescope_phi=0" in command
+    assert show_all in command
+    assert "run000001_proton_za20deg_azm000deg_South_test_layout_test" in command
+
+    _test_corsika_simtel_runner = copy.deepcopy(corsika_simtel_runner)
+    _test_corsika_simtel_runner.label = None
+    command = _test_corsika_simtel_runner._make_run_command(input_file="-", run_number=1)
+    assert "-W" not in command
+
+
+def test_make_run_command_divergent(corsika_simtel_runner, simtel_command, show_all):
+    corsika_simtel_runner.label = "test-corsika-simtel-runner-divergent-pointing"
+    command = corsika_simtel_runner._make_run_command(input_file="-", run_number=1)
+    assert simtel_command in command
+    assert "-W telescope_theta=20" in command  # -W is for pointing
+    assert "-W telescope_phi=0" in command
+    assert show_all in command
+    assert "run000001_proton_za20deg_azm000deg_South_test_layout_test" in command
+
+
+def test_get_file_name(corsika_simtel_runner, simulation_file):
+
+    assert (
+        corsika_simtel_runner.get_file_name(
+            simulation_software="corsika", file_type="log", run_number=1
+        ).name
+        == simulation_file + ".log.gz"
+    )
+
+    simtel_simulation_file = simulation_file + ".simtel.zst"
+
+    assert (
+        corsika_simtel_runner.get_file_name(
+            simulation_software="simtel", file_type="simtel_output", run_number=1
+        ).name
+        == simtel_simulation_file
+    )
+
+    # preference given to simtel runner
+    assert (
+        corsika_simtel_runner.get_file_name(
+            simulation_software=None, file_type="simtel_output", run_number=1
+        ).name
+        == simtel_simulation_file
+    )
+
+    # no simulator_array
+    _test_corsika_simtel_runner = copy.deepcopy(corsika_simtel_runner)
+    _test_corsika_simtel_runner.simulator_array = None
+
+    assert (
+        _test_corsika_simtel_runner.get_file_name(
+            simulation_software=None, file_type="simtel_output", run_number=1
+        ).name
+        == simtel_simulation_file
+    )
diff --git a/tests/unit_tests/runners/test_runner_services.py b/tests/unit_tests/runners/test_runner_services.py
new file mode 100644
index 000000000..d739e20ae
--- /dev/null
+++ b/tests/unit_tests/runners/test_runner_services.py
@@ -0,0 +1,205 @@
+#!/usr/bin/python3
+
+import copy
+import logging
+import pathlib
+import shutil
+
+import pytest
+
+import simtools.runners.runner_services as runner_services
+
+logger = logging.getLogger()
+logger.setLevel(logging.DEBUG)
+
+
+@pytest.fixture()
+def runner_service(corsika_runner):
+    """Runner services object for corsika."""
+    _runner_service = runner_services.RunnerServices(
+        corsika_config=corsika_runner.corsika_config, label="test-corsika-runner"
+    )
+    _runner_service.load_data_directories("corsika")
+    return _runner_service
+
+
+@pytest.fixture()
+def runner_service_config_only(corsika_config):
+    """Runner services object with simplified config."""
+    return runner_services.RunnerServices(
+        corsika_config=corsika_config,
+        label="test-corsika-runner",
+    )
+
+
+@pytest.fixture()
+def file_base_name():
+    """Base name for simulation test file."""
+    return "run000001_proton_za20deg_azm000deg_South_test_layout_test-corsika-runner"
+
+
+def test_init_runner_services(runner_service_config_only):
+    assert runner_service_config_only.label == "test-corsika-runner"
+    assert runner_service_config_only.corsika_config.primary == "proton"
+    assert runner_service_config_only.directory == {}
+
+
+def test_get_info_for_file_name(runner_service_config_only):
+    info_for_file_name = runner_service_config_only._get_info_for_file_name(run_number=1)
+    assert info_for_file_name["run_number"] == 1
+    assert info_for_file_name["primary"] == "proton"
+    assert info_for_file_name["array_name"] == "test_layout"
+    assert info_for_file_name["site"] == "South"
+    assert info_for_file_name["label"] == "test-corsika-runner"
+    assert info_for_file_name["zenith"] == pytest.approx(20)
+    assert info_for_file_name["azimuth"] == pytest.approx(0)
+
+
+def test_get_simulation_software_list(runner_service_config_only):
+    assert runner_service_config_only._get_simulation_software_list("corsika") == ["corsika"]
+    assert runner_service_config_only._get_simulation_software_list("CoRsIka") == ["corsika"]
+    assert runner_service_config_only._get_simulation_software_list("simtel") == ["simtel"]
+    assert runner_service_config_only._get_simulation_software_list("corsika_simtel") == [
+        "corsika",
+        "simtel",
+    ]
+    assert runner_service_config_only._get_simulation_software_list("something_else") == []
+
+
+def test_load_corsika_data_directories(runner_service_config_only):
+    runner_service_config_only.load_data_directories("corsika")
+    assert isinstance(runner_service_config_only.directory, dict)
+
+    for item in runner_service_config_only.directory.values():
+        assert isinstance(item, pathlib.Path)
+
+
+def test_has_file(io_handler, runner_service):
+    corsika_file = io_handler.get_input_data_file(
+        file_name="run1_proton_za20deg_azm0deg_North_1LST_test-lst-array.corsika.zst", test=True
+    )
+    # Copying the corsika file to the expected location and
+    # changing its name for the sake of this test.
+    # This should not affect the efficiency of this test.
+    output_directory = runner_service.directory["data"].joinpath(
+        runner_service._get_run_number_string(1)
+    )
+    output_directory.mkdir(parents=True, exist_ok=True)
+    shutil.copy(
+        corsika_file,
+        output_directory.joinpath(
+            "run000001_proton_za20deg_azm000deg_South_test_layout_test-corsika-runner.zst"
+        ),
+    )
+    assert runner_service.has_file(file_type="corsika_output", run_number=1)
+    assert not runner_service.has_file(file_type="log", run_number=1234)
+
+
+def test_get_file_basename(runner_service, file_base_name):
+    assert runner_service._get_file_basename(1) == file_base_name
+    _runner_service_copy = copy.deepcopy(runner_service)
+    _runner_service_copy.label = ""
+    assert _runner_service_copy._get_file_basename(1) == (
+        "run000001_proton_za20deg_azm000deg_South_test_layout"
+    )
+
+
+def test_get_log_file_path(runner_service, corsika_runner, file_base_name):
+    # log.gz
+    assert runner_service._get_log_file_path("log", file_base_name) == corsika_runner._directory[
+        "logs"
+    ].joinpath(f"{file_base_name}.log.gz")
+
+    # hdata.zst
+    assert runner_service._get_log_file_path(
+        "histogram", file_base_name
+    ) == corsika_runner._directory["logs"].joinpath(f"{file_base_name}.hdata.zst")
+
+
+def test_get_data_file_path(runner_service, corsika_runner, file_base_name):
+    # corsika log
+    assert runner_service._get_data_file_path(
+        file_type="corsika_log", file_name=file_base_name, run_number=1
+    ) == corsika_runner._directory["data"].joinpath(
+        runner_service._get_run_number_string(1)
+    ).joinpath(
+        f"{file_base_name}.log"
+    )
+
+    # corsika output
+    assert runner_service._get_data_file_path(
+        file_type="corsika_output", file_name=file_base_name, run_number=1
+    ) == corsika_runner._directory["data"].joinpath(
+        runner_service._get_run_number_string(1)
+    ).joinpath(
+        f"{file_base_name}.zst"
+    )
+
+    # simtel output
+    assert runner_service._get_data_file_path(
+        file_type="simtel_output", file_name=file_base_name, run_number=1
+    ) == corsika_runner._directory["data"].joinpath(
+        runner_service._get_run_number_string(1)
+    ).joinpath(
+        f"{file_base_name}.simtel.zst"
+    )
+
+
+def test_get_sub_file_path(runner_service, file_base_name, io_handler):
+    script_file_dir = io_handler.get_output_directory("test-corsika-runner").joinpath("scripts")
+    assert runner_service._get_sub_file_path(
+        file_type="script",
+        file_name=file_base_name,
+        mode=None,
+    ) == script_file_dir.joinpath(f"sub_{file_base_name}.sh")
+    assert runner_service._get_sub_file_path(
+        file_type="script",
+        file_name=file_base_name,
+        mode="err",
+    ) == script_file_dir.joinpath(f"sub_{file_base_name}.err")
+
+    log_file_dir = io_handler.get_output_directory("test-corsika-runner").joinpath("sub_logs")
+    assert runner_service._get_sub_file_path(
+        file_type="sub_log",
+        file_name=file_base_name,
+        mode=None,
+    ) == log_file_dir.joinpath(f"sub_{file_base_name}.log")
+
+
+def test_get_file_name(runner_service):
+
+    assert isinstance(runner_service.get_file_name("log", run_number=1), pathlib.Path)
+    assert isinstance(runner_service.get_file_name("output", run_number=1), pathlib.Path)
+    assert isinstance(runner_service.get_file_name("sub_log", run_number=1), pathlib.Path)
+
+    with pytest.raises(ValueError):
+        runner_service.get_file_name("foobar", run_number=1, mode="out")
+
+
+def test_get_run_number_string(runner_service_config_only):
+    run_directory = runner_service_config_only._get_run_number_string(1)
+    assert run_directory == "run000001"
+    run_directory = runner_service_config_only._get_run_number_string(123456)
+    assert run_directory == "run123456"
+    with pytest.raises(ValueError, match=r"^Run number cannot have more than 6 digits"):
+        runner_service_config_only._get_run_number_string(1234567)
+
+
+def test_get_resources(runner_service, caplog):
+    sub_log_file = runner_service.get_file_name(file_type="sub_log", run_number=None, mode="out")
+    with open(sub_log_file, "w", encoding="utf-8") as file:
+        lines_to_write = ["RUNTIME 500\n"]
+        file.writelines(lines_to_write)
+    resources = runner_service.get_resources()
+    assert isinstance(resources, dict)
+    assert "runtime" in resources
+    assert resources["runtime"] == 500
+
+    with open(sub_log_file, "w", encoding="utf-8") as file:
+        lines_to_write = ["SOMETHING ELSE 500\n"]
+        file.writelines(lines_to_write)
+
+    with caplog.at_level(logging.DEBUG):
+        resources = runner_service.get_resources()
+    assert resources["runtime"] is None
+    assert "RUNTIME was not found in run log file" in caplog.text
diff --git a/tests/unit_tests/runners/test_simtel_runner.py b/tests/unit_tests/runners/test_simtel_runner.py
new file mode 100644
index 000000000..2e0aec620
--- /dev/null
+++ b/tests/unit_tests/runners/test_simtel_runner.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python3
+
+import logging
+import re
+from pathlib import Path
+
+import pytest
+
+from simtools.runners.simtel_runner import SimtelExecutionError, SimtelRunner
+
+logger = logging.getLogger()
+logger.setLevel(logging.DEBUG)
+
+
+@pytest.fixture()
+def simtel_runner(simtel_path, corsika_config):
+    return SimtelRunner(simtel_path=simtel_path, label="test", corsika_config=corsika_config)
+
+
+def test_repr(simtel_runner):
+    assert repr(simtel_runner) == "SimtelRunner(label=test)\n"
+
+
+def test_prepare_run_script(simtel_runner, tmp_test_directory):
+    simtel_runner._base_directory = Path(tmp_test_directory)
+    script_file = simtel_runner.prepare_run_script(
+        test=True, input_file="test", run_number=1, extra_commands=None
+    )
+    assert script_file.exists()
+    with open(script_file) as f:
+        script_content = f.read()
+        assert "/usr/bin/env bash" in script_content
+        assert "RUNTIME" in script_content
+        assert "test-1" in script_content
+
+    simtel_runner.runs_per_set = 5
+    script_file = simtel_runner.prepare_run_script(
+        test=False, input_file="test", run_number=5, extra_commands="extra"
+    )
+    with open(script_file) as f:
+        script_content = f.read()
+        assert "test-5" in script_content
+        assert script_content.count("test-5") == 5
+        assert "extra" in script_content
+
+
+def test_run(simtel_runner, caplog):
+    with caplog.at_level(logging.INFO):
+        with pytest.raises(SimtelExecutionError):
+            simtel_runner.run(test=True, input_file="test", run_number=5)
+    assert "Running (test) with command: test-5" in caplog.text
+    simtel_runner.runs_per_set = 5
+    with caplog.at_level(logging.DEBUG):
+        with pytest.raises(SimtelExecutionError):
+            simtel_runner.run(test=False, input_file="test", run_number=5)
+    assert "Running (5x) with command: test-5" in caplog.text
+
+
+def test_run_simtel_and_check_output(simtel_runner):
+
+    with pytest.raises(SimtelExecutionError):
+        simtel_runner._run_simtel_and_check_output("test-5")
+    simtel_runner._run_simtel_and_check_output("echo test")
+
+
+def test_make_run_command(simtel_runner, caplog):
+    with caplog.at_level(logging.DEBUG):
+        assert simtel_runner._make_run_command(input_file="test", run_number=5) == "test-5"
+    assert "make_run_command is being called from the base class" in caplog.text
+
+
+def test_simtel_execution_error(simtel_runner):
+    with pytest.raises(SimtelExecutionError):
+        simtel_runner._raise_simtel_error()
+
+
+def testget_config_option(simtel_runner):
+    assert simtel_runner.get_config_option("test", "value") == " -C test=value"
+    assert simtel_runner.get_config_option("test", "value", weak_option=True) == " -W test=value"
+    assert simtel_runner.get_config_option("test", "value", weak_option=False) == " -C test=value"
+    assert simtel_runner.get_config_option("test") == " -C test"
+
+
+def test_raise_simtel_error(simtel_runner):
+    with pytest.raises(SimtelExecutionError, match="Simtel log file does not exist."):
+        simtel_runner._raise_simtel_error()
+
+
+def test_get_resources(simtel_runner):
+    with pytest.raises(FileNotFoundError):
+        simtel_runner.get_resources()
+
+
+def test_get_file_name(simtel_runner):
+    with pytest.raises(
+        ValueError, match=re.escape("simulation_software (test) is not supported in SimulatorArray")
+    ):
+        simtel_runner.get_file_name(simulation_software="test")
+
+    assert isinstance(
+        simtel_runner.get_file_name(simulation_software="simtel", file_type="output", run_number=3),
+        Path,
+    )
diff --git a/tests/unit_tests/simtel/test_simtel_runner.py b/tests/unit_tests/simtel/test_simtel_runner.py
deleted file mode 100644
index bdecb7e50..000000000
--- a/tests/unit_tests/simtel/test_simtel_runner.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/python3
-
-import logging
-
-import pytest
-
-from simtools.simtel.simtel_runner import SimtelExecutionError, SimtelRunner
-
-logger = logging.getLogger()
-logger.setLevel(logging.DEBUG)
-
-
-@pytest.fixture()
-def simtel_runner(simtel_path):
-    return SimtelRunner(simtel_path=simtel_path)
-
-
-def test_run(simtel_runner):
-    with pytest.raises(RuntimeError):
-        simtel_runner.run()
-
-
-def test_simtel_execution_error(simtel_runner):
-    with pytest.raises(SimtelExecutionError):
-        simtel_runner._raise_simtel_error()
-
-
-def test_config_option(simtel_runner):
-    assert simtel_runner._config_option("test", "value") == " -C test=value"
-    assert simtel_runner._config_option("test", "value", weak_option=True) == " -W test=value"
-    assert simtel_runner._config_option("test", "value", weak_option=False) == " -C test=value"
-    assert simtel_runner._config_option("test") == " -C test"
diff --git a/tests/unit_tests/simtel/test_simulator_array.py b/tests/unit_tests/simtel/test_simulator_array.py
index 762bd175e..3af16bde8 100644
--- a/tests/unit_tests/simtel/test_simulator_array.py
+++ b/tests/unit_tests/simtel/test_simulator_array.py
@@ -1,12 +1,11 @@
 #!/usr/bin/python3
 
 import logging
-import shutil
 from pathlib import Path
 
-import astropy.units as u
 import pytest
 
+from simtools.runners.simtel_runner import InvalidOutputFileError
 from simtools.simtel.simulator_array import SimulatorArray
 
 logger = logging.getLogger()
@@ -14,72 +13,35 @@
 
 
 @pytest.fixture()
-def simtel_runner(array_model_north, simtel_path):
+def simtel_runner(corsika_config, simtel_path):
     return SimulatorArray(
-        array_model=array_model_north,
+        corsika_config=corsika_config,
         simtel_path=simtel_path,
-        config_data={
-            "primary": "proton",
-            "zenith_angle": 20 * u.deg,
-            "azimuth_angle": 0 * u.deg,
-        },
+        label="test-simtel-runner",
+        keep_seeds=False,
+        use_multipipe=False,
     )
 
 
-@pytest.fixture()
-def corsika_file(io_handler):
-    return io_handler.get_input_data_file(
-        file_name="run1_proton_za20deg_azm0deg_North_1LST_test-lst-array.corsika.zst", test=True
-    )
-
-
-def test_run_script(simtel_runner, corsika_file):
-    script = simtel_runner.prepare_run_script(run_number=1, input_file=corsika_file)
-    assert Path(script).exists()
+def test_simtel_runner(simtel_runner):
+    sr = simtel_runner
+    assert "simtel" in str(sr._directory["output"])
+    assert "simtel" in str(sr._directory["data"])
+    assert isinstance(sr._directory["data"], Path)
 
 
-def test_get_info_for_file_name(simtel_runner):
-    info_for_file_name = simtel_runner.get_info_for_file_name(run_number=1)
-    assert info_for_file_name["run"] == 1
-    assert info_for_file_name["primary"] == "proton"
-    assert info_for_file_name["array_name"] == "test_layout"
-    assert info_for_file_name["site"] == "North"
-    assert info_for_file_name["zenith"] == pytest.approx(20)
-    assert info_for_file_name["azimuth"] == pytest.approx(0)
-    assert info_for_file_name["label"] == "test-lst-array"
-
-
-def test_get_file_name(simtel_runner):
-    info_for_file_name = simtel_runner.get_info_for_file_name(run_number=1)
-    file_name = "run000001_proton_za020deg_azm000deg_North_test_layout_test-lst-array"
-    assert simtel_runner.get_file_name(
-        "log", **info_for_file_name
-    ) == simtel_runner._simtel_log_dir.joinpath(f"{file_name}.log.gz")
-    assert simtel_runner.get_file_name(
-        "histogram", **info_for_file_name
-    ) == simtel_runner._simtel_log_dir.joinpath(f"{file_name}.hdata.zst")
-    assert simtel_runner.get_file_name(
-        "output", **info_for_file_name
-    ) == simtel_runner._simtel_data_dir.joinpath(f"{file_name}.simtel.zst")
-    assert simtel_runner.get_file_name(
-        "sub_log", **info_for_file_name
-    ) == simtel_runner._simtel_log_dir.joinpath(f"log_sub_{file_name}.log")
-    assert simtel_runner.get_file_name(
-        "sub_log", **info_for_file_name, mode="out"
-    ) == simtel_runner._simtel_log_dir.joinpath(f"log_sub_{file_name}.out")
-    with pytest.raises(ValueError):
-        simtel_runner.get_file_name("foobar", **info_for_file_name, mode="out")
+def test_make_run_command(simtel_runner):
+    run_command = simtel_runner._make_run_command(
+        run_number=3, input_file="test_make_run_command.inp"
+    )
+    assert "sim_telarray" in run_command
+    assert "-run" in run_command
+    assert "3" in run_command
+    assert "test-simtel-runner.zst" in run_command
+    assert "test_make_run_command.inp" in run_command
 
 
-def test_has_file(simtel_runner, corsika_file):
-    # Copying the corsika file to the expected location and
-    # changing its name for the sake of this test.
-    # This should not affect the efficiency of this test.
-    shutil.copy(
-        corsika_file,
-        simtel_runner._simtel_data_dir.joinpath(
-            "run000001_proton_za020deg_azm000deg_North_test_layout_test-lst-array.simtel.zst"
-        ),
-    )
-    assert simtel_runner.has_file(file_type="output", run_number=1)
-    assert not simtel_runner.has_file(file_type="log", run_number=1234)
+def test_check_run_result(simtel_runner):
+    expected_pattern = r"sim_telarray output file .+ does not exist\."
+    with pytest.raises(InvalidOutputFileError, match=expected_pattern):
+        assert simtel_runner._check_run_result(run_number=3)
diff --git a/tests/unit_tests/simtel/test_simulator_camera_efficiency.py b/tests/unit_tests/simtel/test_simulator_camera_efficiency.py
index 1e6618041..0ced3c97c 100644
--- a/tests/unit_tests/simtel/test_simulator_camera_efficiency.py
+++ b/tests/unit_tests/simtel/test_simulator_camera_efficiency.py
@@ -33,16 +33,6 @@ def simulator_camera_efficiency(camera_efficiency_sst, telescope_model_sst, simt
     )
 
 
-def test_shall_run(simulator_camera_efficiency):
-    """
-    Testing here that the file does not exist because no simulations
-    are run in unit tests. This function is tested for the positive case
-    in the integration tests.
-    """
-
-    assert simulator_camera_efficiency._shall_run()
-
-
 def test_make_run_command(simulator_camera_efficiency):
     command = simulator_camera_efficiency._make_run_command()
 
diff --git a/tests/unit_tests/simtel/test_simulator_light_emission.py b/tests/unit_tests/simtel/test_simulator_light_emission.py
index c7ea265c8..a8de7c289 100644
--- a/tests/unit_tests/simtel/test_simulator_light_emission.py
+++ b/tests/unit_tests/simtel/test_simulator_light_emission.py
@@ -517,6 +517,3 @@ def test_prepare_script(
     ]
     for call_args, expected_content in zip(mock_file.write.call_args_list, expected_calls):
         assert call_args[0][0] == expected_content
-    print(mock_simulator._script_dir)
-    # Ensure correct return value
-    assert str(script_path) == f"{mock_simulator._script_dir}/xyzls-lightemission.sh"
diff --git a/tests/unit_tests/simtel/test_simulator_ray_tracing.py b/tests/unit_tests/simtel/test_simulator_ray_tracing.py
index 813563534..cf8171908 100644
--- a/tests/unit_tests/simtel/test_simulator_ray_tracing.py
+++ b/tests/unit_tests/simtel/test_simulator_ray_tracing.py
@@ -59,16 +59,6 @@ def test_load_required_files(simulator_ray_tracing):
     assert simulator_ray_tracing._stars_file.exists()
 
 
-def test_shall_run(simulator_ray_tracing):
-    """
-    Testing here that the file does not exist because no simulations
-    are run in unit tests. This function is tested for the positive case
-    in the integration tests.
-    """
-
-    assert simulator_ray_tracing._shall_run()
-
-
 def test_make_run_command(simulator_ray_tracing, model_version):
     command = simulator_ray_tracing._make_run_command()
 
diff --git a/tests/unit_tests/test_simulator.py b/tests/unit_tests/test_simulator.py
index f586c5dc2..1c990bd09 100644
--- a/tests/unit_tests/test_simulator.py
+++ b/tests/unit_tests/test_simulator.py
@@ -1,17 +1,17 @@
 #!/usr/bin/python3
 
+import copy
 import logging
 import math
 import shutil
-from copy import copy
 from pathlib import Path
 
 import pytest
 
 import simtools.utils.general as gen
-from simtools.corsika.corsika_runner import CorsikaRunner
-from simtools.corsika_simtel.corsika_simtel_runner import CorsikaSimtelRunner
 from simtools.model.array_model import ArrayModel
+from simtools.runners.corsika_runner import CorsikaRunner
+from simtools.runners.corsika_simtel_runner import CorsikaSimtelRunner
 from simtools.simtel.simulator_array import SimulatorArray
 from simtools.simulator import InvalidRunsToSimulateError, Simulator
 
@@ -19,11 +19,6 @@
 logger.setLevel(logging.DEBUG)
 
 
-@pytest.fixture()
-def label():
-    return "test"
-
-
 @pytest.fixture()
 def input_file_list():
     return ["run1", "abc_run22", "def_run02_and"]
@@ -35,117 +30,102 @@ def corsika_file():
 
 
 @pytest.fixture()
-def array_simulator(
-    label, simulator_config_data_north, io_handler, db_config, model_version, simtel_path
-):
+def simulations_args_dict(corsika_config_data, model_version, simtel_path):
+    """Return a dictionary with the simulation command line arguments."""
+    args_dict = copy.deepcopy(corsika_config_data)
+    args_dict["simulation_software"] = "simtel"
+    args_dict["simtel_path"] = simtel_path
+    args_dict["model_version"] = model_version
+    args_dict["label"] = "test-array-simulator"
+    args_dict["array_layout_name"] = "test_layout"
+    args_dict["site"] = "North"
+    args_dict["keep_seeds"] = False
+    args_dict["run_number_start"] = 1
+    args_dict["nshow"] = 10
+    return args_dict
+
+
+@pytest.fixture()
+def array_simulator(io_handler, db_config, simulations_args_dict):
+    args_dict = copy.deepcopy(simulations_args_dict)
+    args_dict["simulation_software"] = "simtel"
+
     return Simulator(
-        simulation_software="simtel",
-        simulator_source_path=simtel_path,
-        label=label,
-        config_data=simulator_config_data_north,
+        label=args_dict["label"],
+        args_dict=args_dict,
+        submit_command="local",
         mongo_db_config=db_config,
-        model_version=model_version,
     )
 
 
 @pytest.fixture()
-def shower_simulator(
-    label, simulator_config_data_north, io_handler, db_config, model_version, simtel_path
-):
+def shower_simulator(io_handler, db_config, simulations_args_dict):
+    args_dict = copy.deepcopy(simulations_args_dict)
+    args_dict["simulation_software"] = "corsika"
+    args_dict["label"] = "test-shower-simulator"
 
     return Simulator(
-        simulation_software="corsika",
-        simulator_source_path=simtel_path,
-        label=label,
-        config_data=simulator_config_data_north,
+        label=args_dict["label"],
+        args_dict=args_dict,
+        submit_command="local",
         mongo_db_config=db_config,
-        model_version=model_version,
     )
 
 
 @pytest.fixture()
-def shower_array_simulator(
-    label, simulator_config_data_north, io_handler, db_config, model_version, simtel_path
-):
+def shower_array_simulator(io_handler, db_config, simulations_args_dict):
+    args_dict = copy.deepcopy(simulations_args_dict)
+    args_dict["simulation_software"] = "corsika_simtel"
+    args_dict["label"] = "test-shower-array-simulator"
     return Simulator(
-        simulation_software="corsika_simtel",
-        simulator_source_path=simtel_path,
-        label=label,
-        config_data=simulator_config_data_north,
+        label=args_dict["label"],
+        args_dict=args_dict,
+        submit_command="local",
         mongo_db_config=db_config,
-        model_version=model_version,
     )
 
 
-def test_guess_run_from_file(array_simulator):
-    assert array_simulator._guess_run_from_file("run12345_bla_ble") == 12345
+def test_init_simulator(shower_simulator, array_simulator, shower_array_simulator):
 
-    # Invalid run number - returns 1
-    assert array_simulator._guess_run_from_file("run1test2_bla_ble") == 1
-
-    assert array_simulator._guess_run_from_file("abc-run12345_bla_ble") == 12345
-
-    assert array_simulator._guess_run_from_file("run10") == 10
-
-    assert array_simulator._guess_run_from_file("abc-ran12345_bla_ble") == 1
-
-    # add test for 'Error creating output directory'
-    # (not sure how to test a failed mkdir)
+    assert isinstance(shower_simulator._simulation_runner, CorsikaRunner)
+    assert isinstance(shower_array_simulator._simulation_runner, CorsikaSimtelRunner)
+    assert isinstance(array_simulator._simulation_runner, SimulatorArray)
 
 
-def test_set_simulator(array_simulator, shower_simulator, shower_array_simulator):
-    array_simulator.simulation_software = "simtel"
+def test_simulation_software(array_simulator, shower_simulator, shower_array_simulator, caplog):
     assert array_simulator.simulation_software == "simtel"
-
-    shower_simulator.simulation_software = "corsika"
     assert shower_simulator.simulation_software == "corsika"
-
-    shower_array_simulator.simulation_software = "corsika_simtel"
     assert shower_array_simulator.simulation_software == "corsika_simtel"
 
-    with pytest.raises(gen.InvalidConfigDataError):
-        shower_simulator.simulation_software = "this_simulator_is_not_there"
-
-
-def test_load_configuration_and_simulation_model(array_simulator, simulator_config_data_north):
-    array_simulator._load_configuration_and_simulation_model(simulator_config_data_north)
-
-    assert isinstance(array_simulator._corsika_config_data, dict)
-    assert isinstance(array_simulator.array_model, ArrayModel)
-
-
-def test_load_corsika_config_and_model(shower_simulator, simulator_config_data_north):
-    assert shower_simulator.array_model.site == "North"
-
-    assert "site" not in shower_simulator._corsika_config_data
-
-    _temp_config_data = copy(simulator_config_data_north)
-    _temp_config_data["common"].pop("site")
-    try:
-        shower_simulator._load_corsika_config_and_model(_temp_config_data)
-    except KeyError:
-        pytest.fail("Expected no KeyError to be raised")
+    # setting
+    test_array_simulator = copy.deepcopy(array_simulator)
+    test_array_simulator.simulation_software = "corsika"
+    assert test_array_simulator.simulation_software == "corsika"
 
+    with pytest.raises(gen.InvalidConfigDataError):
+        with caplog.at_level(logging.ERROR):
+            test_array_simulator.simulation_software = "this_simulator_is_not_there"
+        assert "Invalid simulation software" in caplog.text
 
-def test_load_sim_tel_config_and_model(
-    array_simulator,
-    shower_array_simulator,
-    simulator_config_data_north,
-):
-    array_simulator._load_sim_tel_config_and_model(simulator_config_data_north)
-    for key in ("data_directory", "zenith_angle", "azimuth_angle", "primary"):
-        assert key in array_simulator.config._fields
 
+def test_initialize_array_model(shower_simulator, db_config):
+    assert isinstance(
+        shower_simulator._initialize_array_model(mongo_db_config=db_config),
+        ArrayModel,
+    )
 
-def test_load_shower_array_config_and_model(shower_array_simulator, simulator_config_data_north):
-    assert shower_array_simulator.array_model.site == "North"
-    assert shower_array_simulator.config.primary == "gamma"
 
-    assert "site" not in shower_array_simulator._corsika_config_data
-    assert (
-        shower_array_simulator.config.data_directory
-        == simulator_config_data_north["common"]["data_directory"]
-    )
+def test_initialize_run_list(shower_simulator, caplog):
+    assert shower_simulator._initialize_run_list() == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+    test_shower_simulator = copy.deepcopy(shower_simulator)
+    test_shower_simulator.args_dict.pop("run_number_start", None)
+    with pytest.raises(KeyError):
+        with caplog.at_level(logging.ERROR):
+            test_shower_simulator._initialize_run_list()
+        assert (
+            "Error in initializing run list (missing 'run_number_start' or 'number_of_runs')"
+            in caplog.text
+        )
 
 
 def test_validate_run_list_and_range(shower_simulator, shower_array_simulator):
@@ -167,7 +147,6 @@ def test_validate_run_list_and_range(shower_simulator, shower_array_simulator):
             3,
             4,
             5,
-            6,
         ]
 
         assert simulator_now._validate_run_list_and_range(run_list=None, run_range=[6, 3]) == []
@@ -178,20 +157,7 @@ def test_validate_run_list_and_range(shower_simulator, shower_array_simulator):
         with pytest.raises(InvalidRunsToSimulateError):
             simulator_now._validate_run_list_and_range(run_list=None, run_range=[3, 4, 5])
 
-
-def test_collect_array_model_parameters(array_simulator, simulator_config_data_north):
-    _rest_data = array_simulator._collect_array_model_parameters(
-        config_data=simulator_config_data_north
-    )
-    assert isinstance(_rest_data, dict)
-
-
-def test_set_simulation_runner(array_simulator, shower_simulator, shower_array_simulator):
-    assert isinstance(array_simulator._simulation_runner, SimulatorArray)
-
-    assert isinstance(shower_simulator._simulation_runner, CorsikaRunner)
-
-    assert isinstance(shower_array_simulator._simulation_runner, CorsikaSimtelRunner)
+        assert simulator_now._validate_run_list_and_range(run_list=5, run_range=None) == [5]
 
 
 def test_fill_results_without_run(array_simulator, input_file_list):
@@ -203,35 +169,39 @@ def test_fill_results_without_run(array_simulator, input_file_list):
     assert array_simulator.runs == [1, 22, 2]
 
 
-def test_submitting(shower_simulator, array_simulator, corsika_file):
-    shower_simulator.test = True
+def test_simulate_shower_simulator(shower_simulator):
+    shower_simulator._test = True
     shower_simulator._submit_command = "local"
     shower_simulator.simulate()
-
-    shower_simulator.print_list_of_output_files()
-    shower_simulator.print_list_of_log_files()
-    shower_simulator.print_list_of_input_files()
-
+    assert len(shower_simulator._results["output"]) > 0
+    assert len(shower_simulator._results["sub_out"]) > 0
     run_script = shower_simulator._simulation_runner.prepare_run_script(run_number=2)
-
     assert Path(run_script).exists()
 
-    array_simulator.test = True
+
+def test_simulate_array_simulator(array_simulator, corsika_file):
+    array_simulator._test = True
     array_simulator._submit_command = "local"
     array_simulator.simulate(input_file_list=corsika_file)
 
-    array_simulator.print_list_of_output_files()
-    array_simulator.print_list_of_log_files()
-    array_simulator.print_list_of_input_files()
+    assert len(array_simulator._results["output"]) > 0
+    assert len(array_simulator._results["sub_out"]) > 0
+
+
+def test_simulate_shower_array_simulator(shower_array_simulator):
+    shower_array_simulator._test = True
+    shower_array_simulator._submit_command = "local"
+    shower_array_simulator.simulate()
 
-    input_files = array_simulator.get_list_of_input_files()
-    assert str(input_files[0]) == str(corsika_file)
+    assert len(shower_array_simulator._results["output"]) > 0
+    assert len(shower_array_simulator._results["sub_out"]) > 0
 
 
 def test_get_runs_and_files_to_submit(
     array_simulator, shower_simulator, shower_array_simulator, input_file_list
 ):
-    assert array_simulator._get_runs_and_files_to_submit(input_file_list=None) == {}
+    with pytest.raises(ValueError, match="No runs to submit."):
+        assert array_simulator._get_runs_and_files_to_submit(input_file_list=None) == {}
 
     assert array_simulator._get_runs_and_files_to_submit(input_file_list=input_file_list) == {
         1: "run1",
@@ -241,8 +211,11 @@ def test_get_runs_and_files_to_submit(
 
     for simulator_now in [shower_simulator, shower_array_simulator]:
         assert simulator_now._get_runs_and_files_to_submit(input_file_list=None) == {
+            1: None,
+            2: None,
             3: None,
             4: None,
+            5: None,
             6: None,
             7: None,
             8: None,
@@ -253,130 +226,98 @@ def test_get_runs_and_files_to_submit(
 
 def test_enforce_list_type(array_simulator):
     assert array_simulator._enforce_list_type(None) == []
-
     assert array_simulator._enforce_list_type([1, 2, 3]) == [1, 2, 3]
-
     assert array_simulator._enforce_list_type(5) == [5]
 
 
+def test_guess_run_from_file(array_simulator, caplog):
+    assert array_simulator._guess_run_from_file("run12345_bla_ble") == 12345
+
+    assert array_simulator._guess_run_from_file("run5test2_bla_ble") == 5
+
+    assert array_simulator._guess_run_from_file("abc-run12345_bla_ble") == 12345
+
+    assert array_simulator._guess_run_from_file("run10") == 10
+
+    # Invalid run number - returns 1
+    with caplog.at_level(logging.WARNING):
+        assert array_simulator._guess_run_from_file("abc-ran12345_bla_ble") == 1
+    assert "Run number could not be guessed from abc-ran12345_bla_ble using run = 1" in caplog.text
+
+
 def test_fill_results(array_simulator, shower_simulator, shower_array_simulator, input_file_list):
     for simulator_now in [array_simulator, shower_array_simulator]:
-        simulator_now._fill_results_without_run(input_file_list)
-        assert len(simulator_now.get_list_of_output_files()) == 3
+        for run_number in [1, 2, 22]:
+            simulator_now._fill_results(input_file_list[1], run_number=run_number)
+        assert len(simulator_now.get_file_list("output")) == 3
         assert len(simulator_now._results["sub_out"]) == 3
-        assert len(simulator_now.get_list_of_log_files()) == 3
-        assert len(simulator_now.get_list_of_input_files()) == 3
-        assert len(simulator_now.get_list_of_histogram_files()) == 3
-        assert simulator_now.get_list_of_input_files()[1] == "abc_run22"
+        assert len(simulator_now.get_file_list("log")) == 3
+        assert len(simulator_now.get_file_list("input")) == 3
+        assert len(simulator_now.get_file_list("hist")) == 3
+        logger.error(simulator_now.get_file_list("input"))
+        assert simulator_now.get_file_list("input")[1] == "abc_run22"
 
-    shower_simulator._fill_results_without_run(input_file_list)
-    assert len(shower_simulator.get_list_of_output_files()) == 3
-    assert len(shower_simulator.get_list_of_log_files()) == 3
-    assert shower_simulator.get_list_of_histogram_files()[1] is None
+    shower_simulator._fill_results(input_file_list[1], run_number=5)
+    assert len(shower_simulator.get_file_list("output")) == 1
+    assert len(shower_simulator.get_file_list("log")) == 1
+    assert shower_simulator.get_file_list("hist")[0] is None
 
 
 def test_get_list_of_files(shower_simulator):
-    assert len(shower_simulator.get_list_of_output_files()) == len(shower_simulator.runs)
-    assert len(shower_simulator.get_list_of_output_files(run_list=[2, 5, 7])) == 10
-    assert len(shower_simulator.get_list_of_output_files(run_range=[1, 4])) == 14
-
+    test_shower_simulator = copy.deepcopy(shower_simulator)
+    test_shower_simulator._results["output"] = ["file_name"] * 10
+    assert len(test_shower_simulator.get_file_list("output")) == len(shower_simulator.runs)
+    assert len(test_shower_simulator.get_file_list("not_a_valid_file_type")) == 0
 
-def test_no_corsika_data(
-    simulator_config_data_north, label, simtel_path, io_handler, db_config, model_version
-):
-    new_shower_config_data = copy(simulator_config_data_north)
-    new_shower_config_data.pop("data_directory", None)
-    new_shower_simulator = Simulator(
-        label=label,
-        simulation_software="corsika",
-        config_data=new_shower_config_data,
-        simulator_source_path=simtel_path,
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
-    files = new_shower_simulator.get_list_of_output_files(run_list=[3])
 
-    assert "_" + label in files[0]
+def test_print_list_of_files(array_simulator, input_file_list, capsys):
+    array_simulator._fill_results_without_run(input_file_list)
+    array_simulator.print_list_of_files("log")
+    captured = capsys.readouterr()
+    assert "log.gz" in captured.out
+    assert captured.out.count("log.gz") == 3
+    array_simulator.print_list_of_files("blabla")
+    assert captured.out.count("blabal") == 0
 
 
-def test_resources(
-    label, simulator_config_data_north, io_handler, db_config, simtel_path, model_version, capsys
-):
-    simulator_config_data_north["showers"]["run_list"] = 1
-    simulator_config_data_north["showers"]["run_range"] = None
-    shower_simulator = Simulator(
-        label="corsika-test",
-        simulation_software="corsika",
-        config_data=simulator_config_data_north,
-        simulator_source_path=simtel_path,
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
+def test_resources(shower_simulator, capsys):
     shower_simulator.resources(input_file_list=None)
     print_text = capsys.readouterr()
-    assert "Walltime/run [sec] = nan" in print_text.out
+    assert "Wall time/run [sec] = nan" in print_text.out
 
 
-def test_make_resources_report(
-    label, simulator_config_data_north, io_handler, db_config, simtel_path, model_version
-):
-    simulator_config_data_north["showers"]["run_list"] = 1
-    simulator_config_data_north["showers"]["run_range"] = None
-    shower_simulator = Simulator(
-        label="corsika-test",
-        simulation_software="corsika",
-        config_data=simulator_config_data_north,
-        simulator_source_path=simtel_path,
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
+def test_make_resources_report(shower_simulator):
     _resources_1 = shower_simulator._make_resources_report(input_file_list=None)
-    assert math.isnan(_resources_1["Walltime/run [sec]"])
+    assert math.isnan(_resources_1["Wall time/run [sec]"])
 
     # Copying the corsika log file to the expected location in the test directory.
-    # This should not affect the efficacy of this test.
     log_file_name = "log_sub_corsika_run000001_gamma_North_test_layout_test-production.out"
     shutil.copy(
         f"tests/resources/{log_file_name}",
         shower_simulator._simulation_runner.get_file_name(
             file_type="sub_log",
-            **shower_simulator._simulation_runner.get_info_for_file_name(1),
+            run_number=1,
             mode="out",
         ),
     )
-    _resources_1 = shower_simulator._make_resources_report(input_file_list=log_file_name)
-    assert _resources_1["Walltime/run [sec]"] == 6
+    test_shower_simulator = copy.deepcopy(shower_simulator)
+    test_shower_simulator.runs = [1]
+    _resources_1 = test_shower_simulator._make_resources_report(input_file_list=log_file_name)
+    assert _resources_1["Wall time/run [sec]"] == 6
 
     with pytest.raises(FileNotFoundError):
-        shower_simulator.runs = [4]
-        shower_simulator._make_resources_report(input_file_list)
+        test_shower_simulator.runs = [4]
+        test_shower_simulator._make_resources_report(input_file_list)
 
 
-def test_get_runs_to_simulate(
-    simulator_config_data_north, simtel_path, io_handler, db_config, model_version
-):
-    shower_simulator = Simulator(
-        label="corsika-test",
-        simulation_software="corsika",
-        config_data=simulator_config_data_north,
-        simulator_source_path=simtel_path,
-        mongo_db_config=db_config,
-        model_version=model_version,
-    )
+def test_get_runs_to_simulate(shower_simulator):
     assert len(shower_simulator.runs) == len(
         shower_simulator._get_runs_to_simulate(run_list=None, run_range=None)
     )
 
     assert 3 == len(shower_simulator._get_runs_to_simulate(run_list=[2, 5, 7]))
 
-    assert 4 == len(shower_simulator._get_runs_to_simulate(run_range=[1, 4]))
+    assert 3 == len(shower_simulator._get_runs_to_simulate(run_range=[1, 4]))
 
     shower_simulator.runs = None
     assert isinstance(shower_simulator._get_runs_to_simulate(), list)
-
-
-def test_print_list_of_files(array_simulator, input_file_list):
-    array_simulator._fill_results_without_run(input_file_list)
-    with pytest.raises(KeyError):
-        array_simulator._print_list_of_files("blabla")
-    array_simulator._print_list_of_files("log")