File indexing completed on 2026-04-28 07:12:52
0001 """Experimental software stack base classes
0002
0003 This module defines a framework for interfacing with a generic experimental
0004 software stack. Components of the stack are represented as layers defined
0005 by a name, a command to be run, and a rule dictating how arguments are
0006 combined. These are layers are collected into a thin container, which
0007 represents the stack.
0008
0009 Key Classes:
0010 - StackLayer: Abstract base class representing a component of a stack
0011 - ExperimentStack: Base container to hold layers representing the stack
0012 """
0013
0014 from abc import ABC, abstractmethod
0015 from dataclasses import dataclass, fields, field
0016 from typing import Dict, List
0017
0018
0019 @dataclass
0020 class StackLayer(ABC):
0021 """Represents a layer of a software stack
0022
0023 Abstract base class that represents a layer of an experimental software
0024 stack. A layer is defined by a unique name, a specific command to be run,
0025 and a rule which dictates how arguments are combined.
0026
0027 Properties:
0028 name: Unique name for the layer.
0029 command: Command to be run (e.g. npsim)
0030 rule: Recipe for combining the command and provided arguments
0031 using keywords, e.g. '{command} {arguments} {inputs} {outputs}'
0032
0033 Example:
0034 >>> def ExperimentLayer(StackLayer):
0035 ... name="sim"
0036 ... command="npsim"
0037 ... rule='{command} {arguments} {inputs} {outputs}"
0038 >>> layer = ExperimentLayer()
0039 >>> run_layer = layer.make_command(
0040 ... inputs,
0041 ... outputs,
0042 ... arguments
0043 ... )
0044 """
0045 @property
0046 @abstractmethod
0047 def name(self):
0048 """Name of this layer (e.g. Sim)
0049 """
0050 pass
0051
0052 @property
0053 @abstractmethod
0054 def command(self):
0055 """Command to be run (e.g. npsim)
0056 """
0057 pass
0058
0059 @property
0060 @abstractmethod
0061 def rule(self):
0062 """Recipe for combining command and arguments
0063 """
0064 pass
0065
0066 @abstractmethod
0067 def _make_input_arg(self, inputs: List[str]) -> str:
0068 """Make input argument
0069
0070 Converts provided list of input filee into string of properly formatted
0071 inputs for command.
0072
0073 Args:
0074 inputs: List of input files
0075 Returns:
0076 String of formatted input files
0077 """
0078 pass
0079
0080 @abstractmethod
0081 def _make_output_arg(self, outputs: List[str]) -> str:
0082 """Make output argument
0083
0084 Converts provided list of output files into string of properly formatted
0085 outputs for command.
0086
0087 Args:
0088 outputs: List of output files
0089 Returns:
0090 String of formatted outputs
0091 """
0092 pass
0093
0094 def _make_other_arg(self, arguments: List[str]) -> str:
0095 """Make other arguments
0096
0097 By default, joins provided list of arguments into a space-separated
0098 string. Can be overwritten for behavior unique to specific layers.
0099
0100 Args:
0101 arguments: List of arguments to join
0102 Returns:
0103 String of formatted arguments
0104 """
0105 return ' '.join(arguments)
0106
0107 def make_command(self, inputs: str, outputs: str, arguments:str = None) -> str:
0108 """Make command
0109
0110 Returns command to run with all inputs outputs, and arguments formatted
0111 according to layer rule.
0112
0113 Args:
0114 inputs: List of input files
0115 outputs: List of output files
0116 arguments: Optional ist of additional arguments
0117 """
0118
0119 in_arg = self._make_input_arg(inputs)
0120 out_arg = self._make_output_arg(outputs)
0121 command = self.rule.replace('{command}', self.command)
0122 command = command.replace('{inputs}', in_arg)
0123 command = command.replace('{outputs}', out_arg)
0124
0125
0126 if arguments != None:
0127 other_arg = self._make_other_arg(arguments)
0128 command = command.replace('{arguments}', other_arg)
0129 else:
0130 command = command.replace('{arguments}', '')
0131
0132
0133
0134 return command.replace(' ', ' ')
0135
0136
0137 class AnaLayer(StackLayer):
0138 """Represents a generic analysis layer of a software stack
0139
0140 Subclass derived from the abstract StackLayer to represent a generic
0141 analysis layer of an experimental software stack, in which users will
0142 run code they provide.
0143
0144 Example:
0145 >>> layer = AnaLayer()
0146 >>> layer.command="do_my_analysis.py"
0147 >>> layer.rule='{command} {arguments} -i {inputs} -o {outputs}'
0148 >>> run_layer = layer.make_command(
0149 ... inputs,
0150 ... outputs,
0151 ... arguments
0152 ... )
0153 """
0154 name = "ana"
0155 command = ""
0156 rule = ''
0157
0158
0159
0160 def _make_input_arg(self, inputs: List[str]) -> str:
0161 """Formats inputs for generic analysis layer"""
0162 return ' '.join(inputs)
0163
0164
0165
0166 def _make_output_arg(self, outputs: List[str]) -> str:
0167 """Formats outputs for generic analysis layer"""
0168 return ' '.join(outputs)
0169
0170
0171 @dataclass
0172 class ExperimentStack(ABC):
0173 """Represents an experimental software stack
0174
0175 Abstract base class that represents an experimental software as a
0176 dictionary of layers keyed on the layer names.
0177
0178 Properties:
0179 layers: Dictionary of layers
0180
0181 Example:
0182 >>> def MySimLayer(StackLayer):
0183 ... name="sim"
0184 ... command="dosim"
0185 ... rule='{command} {arguments} -I {inputs} -O {outputs}'
0186 ... @dataclass
0187 >>> def MyExperimentStack(ExperimentStack):
0188 ... sim: MySimLayer = field(default_factory = MySimLayer)
0189 >>> stack = MyExperimentStack()
0190 >>> dosim = stack["sim"].make_command(
0191 ... inputs,
0192 ... outputs,
0193 ... arguments
0194 ... )
0195 """
0196 layers: Dict[str, StackLayer] = field(init = False, repr = False)
0197
0198 def __post_init__(self):
0199 """
0200 Automatically adds fields that are StackLayer instances
0201 and adds them to the dictionary.
0202 """
0203 self.layers = {
0204 obj.name: obj
0205 for f in fields(self)
0206 if f.init and isinstance((obj := getattr(self, f.name)), StackLayer)
0207 }
0208
0209 def __getitem__(self, key) -> StackLayer:
0210 """Retrieve the layer identified by key"""
0211 return self.layers[key]