class TestOptionsExpander(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.ops = Options("op1") self.ops.beam.energy_eV = [5e3, 10e3, 15e3] self.expander = OptionsExpander() def tearDown(self): unittest.TestCase.tearDown(self) def testexpand(self): opss = self.expander.expand(self.ops) self.assertEqual(3, len(opss)) names = list(map(attrgetter('name'), opss)) self.assertIn('op1+energy_eV=5000.0', names) self.assertIn('op1+energy_eV=10000.0', names) self.assertIn('op1+energy_eV=15000.0', names) def testis_expandable(self): self.assertTrue(self.expander.is_expandable(self.ops)) self.ops.beam.energy_eV = [5e3] self.assertFalse(self.expander.is_expandable(self.ops))
class Converter(object): """ Base class of all converters. Derived class shall modify the following class variables to specify the allowable classes for each of these parameters: * :attr:`MATERIALS`: list of allowable material classes * :attr:`BEAM`: list of allowable beam classes * :attr:`GEOMETRIES`: list of allowable geometry classes * :attr:`DETECTORS`: list of allowable detector classes * :attr:`LIMITS`: list of allowable limit classes * :attr:`MODELS`: dictionary of allowable models (key: model type, value: list of allowable models) * :attr:`DEFAULT_MODELS`: dictionary of default models (key: model type, value: default model) .. note:: The keys for :attr:`MODELS` and :attr:`DEFAULT_MODELS` have to be the same. """ PARTICLES = [] MATERIALS = [] BEAMS = [] GEOMETRIES = [] DETECTORS = [] LIMITS = [] MODELS = {} DEFAULT_MODELS = {} def __init__(self): self._expander = OptionsExpander() def _warn(self, *messages): message = ' '.join(messages) warnings.warn(message, ConversionWarning) def convert(self, options): """ Performs two tasks: 1. Expand the options to create single options that the Monte Carlo program can run 2. Converts the beam, geometry, detectors and limits to be compatible with the Monte Carlo program. This method may raise :exc:`ConversionException` if some parameters cannot be converted. If a parameter is modified or removed during the conversion, a :class:`ConversionWarning` is issued. If no conversion is required, the parameter (and its object) are left intact. This method returns a list of options, corresponding to the expanded options. :arg options: options """ list_options = [] for options in self._expand(options): if not self._convert_beam(options): continue if not self._convert_geometry(options): continue if not self._convert_detectors(options): continue if not self._convert_limits(options): continue if not self._convert_models(options): continue list_options.append(options) if not list_options: self._warn("No options could be converted") return list_options def _expand(self, options): return self._expander.expand(options) def _convert_beam(self, options): clasz = options.beam.__class__ if clasz not in self.BEAMS: self._warn("Cannot convert beam (%s)." % clasz.__name__, "This options definition was removed.") return False if options.beam.particle not in self.PARTICLES: self._warn("Particle (%s) not supported" % options.beam.particle, "This options definition was removed.") return False return True def _convert_geometry(self, options): # Check class clasz = options.geometry.__class__ if clasz not in self.GEOMETRIES: self._warn("Cannot convert geometry (%s)." % clasz.__name__, "This options definition was removed.") return False # Check material for material in options.geometry.get_materials(): if not isinstance(material, tuple(self.MATERIALS)): self._warn("Invalid material (%s) in geometry (%s)" % \ (material.name, clasz.__name__), "This options definition was removed.") return False return True def _convert_detectors(self, options): for key in list(options.detectors.keys()): detector = options.detectors[key] clasz = detector.__class__ if clasz not in self.DETECTORS: options.detectors.pop(key) self._warn("Detector '%s' of type '%s' cannot be converted." % \ (key, clasz.__name__), "It was removed") if not options.detectors: self._warn("No detectors in options.", "This options definition was removed.") return False return True def _convert_limits(self, options): for limit in list(options.limits): clasz = limit.__class__ if clasz not in self.LIMITS: options.limits.discard(limit) self._warn("Limit of type '%s' cannot be converted." % clasz.__name__, "It was removed") if not options.limits: self._warn("No limits in options.", "This options definition was removed.") return False return True def _convert_models(self, options): for model_type, default_model in self.DEFAULT_MODELS.items(): models = list(options.models.iterclass(model_type)) # Add default model if model type is missing if not models: options.models.add(default_model) self._warn("Set default model (%s) for model type '%s'" % \ (default_model, model_type)) # Check if model is allowable else: availables = set(self.MODELS[model_type]) classes = set(filter(inspect.isclass, availables)) for model in models: if model not in availables and \ not isinstance(model, tuple(classes)): options.models.discard(model) # not required options.models.add(default_model) self._warn("Model (%s) is not allowable." % model, "It is replaced by the default model (%s)." % default_model) # Remove extra model types for model in list(options.models): if model.type not in self.DEFAULT_MODELS: options.models.discard(model) self._warn("Unknown model type (%s) for this converter." % model.type, "Model (%s) is removed." % model) return True