Back to home page

EIC code displayed by LXR

 
 

    


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         # format and sub in inputs/outputs
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         # if needed, sub in any other arguments
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         # return formatted command without any
0133         # stray double spaces
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     # FIXME should allow for users to specify how to
0159     # handle multiple inputs
0160     def _make_input_arg(self, inputs: List[str]) -> str:
0161         """Formats inputs for generic analysis layer"""
0162         return ' '.join(inputs)
0163 
0164     # FIXME sould allow for users to specify how to
0165     # handle multiple outputs
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]