def test_iterate_over_interventions(self, get_intervention): """Test __iter___ method of AssetRegister class """ asset_one = get_intervention register = InterventionRegister() register.register(asset_one) for asset in register: assert asset.sha1sum() == asset_one.sha1sum()
def test_add_duplicate_intervention(self, get_intervention): """Tests that only unique interventions are retained """ asset_one = get_intervention asset_two = get_intervention register = InterventionRegister() register.register(asset_one) register.register(asset_two) assert len(register) == 1
def test_retrieve_intervention_by_name(self, get_intervention): water_treatment_plant = get_intervention register = InterventionRegister() register.register(water_treatment_plant) # pick an asset from the list - this is what the optimiser will do asset = register.get_intervention('water_treatment_plant') assert asset.name == "water_treatment_plant" assert asset.data == { 'name': 'water_treatment_plant', 'sector': 'water_supply', 'capacity': { 'units': 'ML/day', 'value': 5 }, 'location': 'oxford' }
def __init__(self, name): # housekeeping super().__init__(name) self.logger = logging.getLogger(__name__) self.max_iterations = 25 self.convergence_relative_tolerance = 1e-05 self.convergence_absolute_tolerance = 1e-08 # models - includes types of SectorModel and ScenarioModel self.dependency_graph = networkx.DiGraph() # systems, interventions and (system) state self.timesteps = [] self.interventions = InterventionRegister() self.initial_conditions = [] self.planning = Planning([]) self._state = defaultdict(dict) # scenario data and results self._results = defaultdict(dict)
def build_register_two(build_intervention_es, build_intervention_ws): water = Intervention(data=build_intervention_ws) energy = Intervention(data=build_intervention_es) register = InterventionRegister() register.register(energy) register.register(water) return register
def test_error_when_retrieve_by_name(self, get_intervention): water_treatment_plant = get_intervention register = InterventionRegister() register.register(water_treatment_plant) with raises(ValueError) as excinfo: register.get_intervention('not_here') expected = "Intervention 'not_here' not found in register" assert str(excinfo.value) == expected
def test_register_intervention(self, get_intervention): water_treatment_plant = get_intervention register = InterventionRegister() register.register(water_treatment_plant) assert len(register) == 1 assert sorted(register._attribute_keys) == [ "capacity", "location", "name", "sector" ] attr_idx = register.attribute_index("name") possible = register._attribute_possible_values[attr_idx] assert possible == [None, "water_treatment_plant"] attr_idx = register.attribute_index("capacity") possible = register._attribute_possible_values[attr_idx] assert possible == [None, {'units': 'ML/day', 'value': 5}]
def test_register_len_one(self, build_intervention_ws): water = Intervention(data=build_intervention_ws) register = InterventionRegister() register.register(water) assert len(register) == 1
class SosModel(CompositeModel): """Consists of the collection of models joined via dependencies This class is populated at runtime by the :class:`SosModelBuilder` and called from :func:`smif.cli.run_model`. SosModel inherits from :class:`smif.composite.Model`. Arguments --------- name : str The unique name of the SosModel """ def __init__(self, name): # housekeeping super().__init__(name) self.logger = logging.getLogger(__name__) self.max_iterations = 25 self.convergence_relative_tolerance = 1e-05 self.convergence_absolute_tolerance = 1e-08 # models - includes types of SectorModel and ScenarioModel self.dependency_graph = networkx.DiGraph() # systems, interventions and (system) state self.timesteps = [] self.interventions = InterventionRegister() self.initial_conditions = [] self.planning = Planning([]) self._state = defaultdict(dict) # scenario data and results self._results = defaultdict(dict) def add_model(self, model): """Adds a sector model to the system-of-systems model Arguments --------- model : :class:`smif.sector_model.SectorModel` A sector model wrapper """ assert isinstance(model, Model) self.logger.info("Loading model: %s", model.name) self.models[model.name] = model @property def results(self): """Get nested dict of model results Returns ------- dict Nested dictionary in the format results[str:model][str:parameter] """ # convert from defaultdict to plain dict return dict(self._results) def simulate(self, timestep, data=None): """Run the SosModel Returns ------- results : dict Nested dict keyed by model name, parameter name """ self.check_dependencies() run_order = self._get_model_sets_in_run_order() self.logger.info("Determined run order as %s", [x.name for x in run_order]) results = {} for model in run_order: # get data for model # TODO settle and test data dict structure/object between simple/composite models sim_data = {} for input_name, dep in model.deps.items(): input_ = model.model_inputs[input_name] if input_ in self.free_inputs: # pick external dependencies from data param_data = data[dep.source_model.name][dep.source.name] else: # pick internal dependencies from results param_data = results[dep.source_model.name][ dep.source.name] param_data_converted = dep.convert(param_data, input_) sim_data[input_.name] = param_data_converted sim_data = self._get_parameter_values(model, sim_data, data) sim_results = model.simulate(timestep, sim_data) for model_name, model_results in sim_results.items(): results[model_name] = model_results return results def check_dependencies(self): """For each contained model, compare dependency list against list of available models and build the dependency graph """ if self.free_inputs.names: msg = "A SosModel must have all inputs linked to dependencies." \ "Define dependencies for %s" raise NotImplementedError(msg, ", ".join(self.free_inputs.names)) for model in self.models.values(): if isinstance(model, SosModel): msg = "Nesting of SosModels not yet supported" raise NotImplementedError(msg) else: self.dependency_graph.add_node(model, name=model.name) for sink, dependency in model.deps.items(): provider = dependency.source_model msg = "Dependency '%s' provided by '%s'" self.logger.debug(msg, sink, provider.name) self.dependency_graph.add_edge(provider, model, { 'source': dependency.source, 'sink': sink }) def get_decisions(self, model, timestep): """Gets the interventions that correspond to the decisions Parameters ---------- model: :class:`smif.sector_model.SectorModel` The instance of the sector model wrapper to run timestep: int The current model year TODO: Move into DecisionManager class """ self.logger.debug("Finding decisions for %i", timestep) current_decisions = [] for decision in self.planning.planned_interventions: if decision['build_date'] <= timestep: name = decision['name'] if name in model.intervention_names: msg = "Adding decision '%s' to instruction list" self.logger.debug(msg, name) intervention = self.interventions.get_intervention(name) current_decisions.append(intervention) # for decision in self.planning.get_rule_based_interventions(timestep): # current_decisions.append(intervention) # for decision in self.planning.get_optimised_interventions(timestep): # current_decisions.append(intervention) return current_decisions def get_state(self, model, timestep): """Gets the state to pass to SectorModel.simulate """ if model.name not in self._state[timestep]: self.logger.warning("Found no state for %s in timestep %s", model.name, timestep) return [] return self._state[timestep][model.name] def set_state(self, model, from_timestep, state): """Sets state output from model ready for next timestep """ for_timestep = self.timestep_after(from_timestep) self._state[for_timestep][model.name] = state def set_data(self, model, timestep, results): """Sets results output from model as data available to other/future models Stores only latest estimated results (i.e. not holding on to iterations here while trying to solve interdependencies) """ self._results[timestep][model.name] = results def _get_model_sets_in_run_order(self): """Returns a list of :class:`Model` in a runnable order. If a set contains more than one model, there is an interdependency and and we attempt to run the models to convergence. Returns ------- list A list of `smif.model.Model` objects """ if networkx.is_directed_acyclic_graph(self.dependency_graph): # topological sort gives a single list from directed graph, currently # ignoring opportunities to run independent models in parallel run_order = networkx.topological_sort(self.dependency_graph, reverse=False) # list of Models (typically ScenarioModel and SectorModel) ordered_sets = list(run_order) else: # contract the strongly connected components (subgraphs which # contain cycles) into single nodes, producing the 'condensation' # of the graph, where each node maps to one or more sector models condensation = networkx.condensation(self.dependency_graph) # topological sort of the condensation gives an ordering of the # contracted nodes, whose 'members' attribute refers back to the # original dependency graph ordered_sets = [] for node_id in networkx.topological_sort(condensation, reverse=False): models = condensation.node[node_id]['members'] if len(models) == 1: ordered_sets.append(models.pop()) else: ordered_sets.append( ModelSet(models, max_iterations=self.max_iterations, relative_tolerance=self. convergence_relative_tolerance, absolute_tolerance=self. convergence_absolute_tolerance)) return ordered_sets def determine_running_mode(self): """Determines from the config in what mode to run the model Returns ======= :class:`RunMode` The mode in which to run the model """ number_of_timesteps = len(self.timesteps) if number_of_timesteps > 1: # Run a sequential simulation mode = RunMode.sequential_simulation elif number_of_timesteps == 0: raise ValueError("No timesteps have been specified") else: # Run a single simulation mode = RunMode.static_simulation return mode def timestep_before(self, timestep): """Returns the timestep previous to a given timestep, or None Arguments --------- timestep : str Returns ------- str """ return element_before(timestep, self.timesteps) def timestep_after(self, timestep): """Returns the timestep after a given timestep, or None Arguments --------- timestep : str Returns ------- str """ return element_after(timestep, self.timesteps) @property def intervention_names(self): """Names (id-like keys) of all known asset type """ return [intervention.name for intervention in self.interventions] @property def sector_models(self): """The list of sector model names Returns ======= list A list of sector model names """ return [ x for x, y in self.models.items() if isinstance(y, SectorModel) ] @property def scenario_models(self): """The list of scenario model names Returns ------- list A list of scenario model names """ return [ x for x, y in self.models.items() if isinstance(y, ScenarioModel) ]