Source code for runners.chain_runner
"""Run script for a chain of simulation stages."""
import logging
from pathlib import Path
from simtools.runners.stages import STDIN, StreamTo
[docs]
class ChainRunner:
"""
Write the run script of a simulation chain.
Stages that both stream are connected by a stream (the upstream stage receives the
downstream commands); otherwise the upstream output file is the downstream input.
A stage may run as several instances (e.g. one per array model).
Parameters
----------
chain : SimulationChain
Ordered simulation stages.
inputs : StageInputs
Array models, simulation parameters, shower configurations and arguments.
label : str
Instance label.
"""
def __init__(self, chain, inputs, label=None):
"""Initialize ChainRunner."""
self._logger = logging.getLogger(__name__)
self.chain = chain
self.stages = [
stage_class.instances(inputs, label=label) for stage_class in chain.stage_classes
]
self.file_list = None
[docs]
def prepare_run(self, run_number, sub_script, input_file=None, extra_commands=None):
"""
Write the run script for one run.
Parameters
----------
run_number : int
Run number.
sub_script : str or Path
Run script to write.
input_file : str or Path, optional
Input file of the first stage (when it does not generate showers).
extra_commands : str or list[str], optional
Commands written before the simulation commands.
"""
lines = self._stage_lines(run_number, input_file)
with Path(sub_script).open("w", encoding="utf-8") as file:
file.write("#!/usr/bin/env bash\n")
file.write("set -e\n")
file.write("set -o pipefail\n")
if extra_commands:
file.write("\n# Writing extras\n")
for line in [extra_commands] if isinstance(extra_commands, str) else extra_commands:
file.write(f"{line}\n")
file.write("# End of extras\n\n")
for line in lines:
file.write(f"{line}\n")
self.file_list = self._merged_file_list()
def _stage_lines(self, run_number, input_file):
"""Return the shell lines of all stages, connecting them by streams or files."""
lines = []
stream = None
for index in reversed(range(len(self.stages))):
streamed_input = index > 0 and self._streams(index - 1, index)
stage_inputs = self._stage_inputs(index, run_number, input_file, streamed_input)
stage_lines = [
line
for instance, stage_input in zip(self.stages[index], stage_inputs)
for line in instance.prepare(run_number, input_file=stage_input, output=stream)
]
if streamed_input:
stream = StreamTo(tuple(stage_lines))
else:
stream = None
lines = stage_lines + lines
return lines
def _streams(self, upstream, downstream):
"""Return whether two neighbouring stages exchange their product as a stream."""
return all(
instance.streams for index in (upstream, downstream) for instance in self.stages[index]
)
def _stage_inputs(self, index, run_number, input_file, streamed_input):
"""Return the input of each instance of a stage."""
count = len(self.stages[index])
if index == 0:
return [input_file] * count
if streamed_input:
return [STDIN] * count
return self._upstream_files(index, run_number)
def _upstream_files(self, index, run_number):
"""
Return the input file of each instance of a stage fed by file hand-over.
A single upstream instance feeds every downstream instance. Otherwise instances are
paired in order, which requires the same number of instances in both stages.
"""
upstream = self.stages[index - 1]
count = len(self.stages[index])
if len(upstream) == 1:
return [upstream[0].output_file(run_number)] * count
if len(upstream) == count:
return [instance.output_file(run_number) for instance in upstream]
upstream_name = self.chain.stage_classes[index - 1].name
downstream_name = self.chain.stage_classes[index].name
raise ValueError(
f"Stage {downstream_name!r} has {count} instances, but upstream stage "
f"{upstream_name!r} has {len(upstream)}. Files are handed over only from one "
"upstream instance or between stages with the same number of instances."
)
def _merged_file_list(self):
"""Merge the file lists of all stage instances (lists for repeated file types)."""
merged = {}
for instance in (instance for instances in self.stages for instance in instances):
for key, data in (instance.file_list or {}).items():
if key not in merged:
merged[key] = data
continue
if not isinstance(merged[key], list):
merged[key] = [merged[key]]
merged[key].extend(data if isinstance(data, list) else [data])
return merged
[docs]
def environment(self):
"""Return the environment variables of all stages."""
environment = {}
for instances in self.stages:
environment.update(instances[0].environment())
return environment
[docs]
def get_resources(self, runtime=None):
"""Return computing resources used (reported by the first stage)."""
return self.stages[0][0].get_resources(runtime=runtime)