<commit_before>"""The decision module handles the three planning levels

Currently, only pre-specified planning is implemented.

The choices made in the three planning levels influence the set of interventions
and assets available within a model run.

The interventions available in a model run are stored in the
:class:`~smif.decision.intervention.InterventionRegister`.

When pre-specified planning are declared, each of the corresponding
interventions in the InterventionRegister are moved to the BuiltInterventionRegister.

Once pre-specified planning is instantiated, the action space for rule-based and
optimisation approaches can be generated from the remaining Interventions in the
InterventionRegister.

"""
__author__ = "<NAME>, <NAME>"
__copyright__ = "<NAME>, <NAME>"
__license__ = "mit"

from abc import ABCMeta, abstractmethod
from logging import getLogger


class DecisionManager(object):
    """A DecisionManager is initialised with one or more model run strategies that refer to
    DecisionModules such as pre-specified planning, a rule-based models or multi-objective
    optimisation. These implementations influence the combination and ordering of decision
    iterations and model timesteps that need to be performed by the model runner.

    The DecisionManager presents a simple decision loop interface to the model runner, in the
    form of a generator which allows the model runner to iterate over the collection of
    independent simulations required at each step.

    A DecisionManager collates the output of the decision algorithms and
    writes the post-decision state through a DataHandle. This allows Models to access a given
    decision state (identified uniquely by timestep and decision iteration id).

    A DecisionManager passes a DataHandle down to a
    DecisionModule, allowing the DecisionModule to access model results from previous timesteps
    and decision iterations when making decisions.

    Arguments
    ---------
    store: smif.data_layer.data_interface.DataInterface
        An instance of a data handle activated for the SosModel
    """

    def __init__(self, store, timesteps, modelrun_name, sos_model_name):

        self.logger = getLogger(__name__)

        self._store = store
        self._modelrun_name = modelrun_name
        self._timesteps = timesteps
        self._decision_modules = []

        self.register = {}
        for sector_model in self._store.read_sos_model(sos_model_name)['sector_models']:
            self.register.update(self._store.read_interventions(sector_model))

        self._set_up_decision_modules(modelrun_name)

    def _set_up_decision_modules(self, modelrun_name):

        # Read in the historical interventions (initial conditions) directly
        initial_conditions = self._store.read_all_initial_conditions(modelrun_name)

        # Read in strategies
        strategies = self._store.read_strategies(modelrun_name)

        self.logger.info("%s strategies found", len(strategies))
        planned_interventions = []
        planned_interventions.extend(initial_conditions)

        for index, strategy in enumerate(strategies):
            # Extract pre-specified planning interventions
            if strategy['strategy'] == 'pre-specified-planning':

                msg = "Adding %s planned interventions to pre-specified-planning %s"
                self.logger.info(msg, len(strategy['interventions']), index)

                planned_interventions.extend(strategy['interventions'])

        # Create a Pre-Specified planning decision module with all
        # the planned interventions

        if planned_interventions:
            self._decision_modules.append(
                PreSpecified(self._timesteps, self.register, planned_interventions)
                )

    def decision_loop(self):
        """Generate bundles of simulation steps to run.

        Each iteration returns a dict: {decision_iteration (int) => list of timesteps (int)}

        With only pre-specified planning, there is a single step in the loop, with a single
        decision iteration with timesteps covering the entire model horizon.

        With a rule based approach, there might be many steps in the loop, each with a single
        decision iteration and single timestep, moving on once some threshold is satisfied.

        With a genetic algorithm, there might be a configurable number of steps in the loop,
        each with multiple decision iterations (one for each member of the algorithm's
        population) and timesteps covering the entire model horizon.

        Implicitly, if the bundle returned in an iteration contains multiple decision
        iterations, they can be performed in parallel. If each decision iteration contains
        multiple timesteps, they can also be parallelised, so long as there are no temporal
        dependencies.
        """
        if len(self._decision_modules) > 0:
            for module in self._decision_modules:
                yield module._get_next_decision_iteration()
        else:
            yield {0: [x for x in self._timesteps]}

    def get_decision(self, data_handle, timestep, iteration):
        """Return all interventions built in the given timestep

        for the given decision
        iteration.

        Arguments
        ---------
        data_handle : smif.data_layer.data_handle.DataHandle
        timestep : int
            A timestep (planning year)
        iteration : int
            A decision iteration

        """
        decisions = []
        for module in self._decision_modules:
            decisions.extend(module.get_decision(data_handle, timestep, iteration))
        self.logger.debug(
            "Retrieved %s decisions from %s",
            len(decisions), str(self._decision_modules))

        self.logger.debug(
            "Writing state for timestep %s and interation %s",
            timestep,
            iteration)
        self._store.write_state(decisions,
                                data_handle._modelrun_name,
                                timestep, iteration)


class DecisionModule(metaclass=ABCMeta):
    """Abstract class which provides the interface to decision mechanisms.

    These mechanisms including Pre-Specified Planning, a Rule-based Approach and
    Multi-objective Optimisation.

    This class provides two main public methods, ``__next__`` which is normally
    called implicitly as a call to the class as an iterator, and ``get_decision()``
    which takes as arguments a smif.model.Model object, and ``timestep`` and
    ``decision_iteration`` integers. The first of these returns a dict of
    decision_iterations and timesteps over which a SosModel should be iterated.
    The latter provides a means to furnish the structure of contained Model
    objects through a list of historical and recent interventions.

    Arguments
    ---------
    timesteps : list
        A list of planning timesteps
    register : smif.interventions.InterventionRegister
        Reference to a fully populated intervention register

    """
    def __init__(self, timesteps, register):
        self.timesteps = timesteps
        self.register = register

    def __next__(self):
        return self._get_next_decision_iteration()

    @abstractmethod
    def _get_next_decision_iteration(self):
        """Implement to return the next decision iteration

        Within a list of decision-iteration/timestep pairs, the assumption is
        that all decision iterations can be run in parallel
        (otherwise only one will be returned) and within a decision interation,
        all timesteps may be run in parallel as long as there are no
        inter-timestep state transitions required (e.g. reservoir level)

        Returns
        -------
        dict
            Yields a dictionary keyed by decision iteration (int) whose values contain
            a list of timesteps
        """
        raise NotImplementedError<commit_msg>Tidied up doc references to intervention register<commit_after>"""The decision module handles the three planning levels

Currently, only pre-specified planning is implemented.

The choices made in the three planning levels influence the set of interventions
and assets available within a model run.

The interventions available in a model run are stored in a dict keyed by name.

"""
__author__ = "<NAME>, <NAME>"
__copyright__ = "<NAME>, <NAME>"
__license__ = "mit"

from abc import ABCMeta, abstractmethod
from logging import getLogger


class DecisionManager(object):
    """A DecisionManager is initialised with one or more model run strategies that refer to
    DecisionModules such as pre-specified planning, a rule-based models or multi-objective
    optimisation. These implementations influence the combination and ordering of decision
    iterations and model timesteps that need to be performed by the model runner.

    The DecisionManager presents a simple decision loop interface to the model runner, in the
    form of a generator which allows the model runner to iterate over the collection of
    independent simulations required at each step.

    A DecisionManager collates the output of the decision algorithms and
    writes the post-decision state through a DataHandle. This allows Models to access a given
    decision state (identified uniquely by timestep and decision iteration id).

    A DecisionManager passes a DataHandle down to a
    DecisionModule, allowing the DecisionModule to access model results from previous timesteps
    and decision iterations when making decisions.

    Arguments
    ---------
    store: smif.data_layer.data_interface.DataInterface
        An instance of a data handle activated for the SosModel
    """

    def __init__(self, store, timesteps, modelrun_name, sos_model_name):

        self.logger = getLogger(__name__)

        self._store = store
        self._modelrun_name = modelrun_name
        self._timesteps = timesteps
        self._decision_modules = []

        self.register = {}
        for sector_model in self._store.read_sos_model(sos_model_name)['sector_models']:
            self.register.update(self._store.read_interventions(sector_model))

        self._set_up_decision_modules(modelrun_name)

    def _set_up_decision_modules(self, modelrun_name):

        # Read in the historical interventions (initial conditions) directly
        initial_conditions = self._store.read_all_initial_conditions(modelrun_name)

        # Read in strategies
        strategies = self._store.read_strategies(modelrun_name)

        self.logger.info("%s strategies found", len(strategies))
        planned_interventions = []
        planned_interventions.extend(initial_conditions)

        for index, strategy in enumerate(strategies):
            # Extract pre-specified planning interventions
            if strategy['strategy'] == 'pre-specified-planning':

                msg = "Adding %s planned interventions to pre-specified-planning %s"
                self.logger.info(msg, len(strategy['interventions']), index)

                planned_interventions.extend(strategy['interventions'])

        # Create a Pre-Specified planning decision module with all
        # the planned interventions

        if planned_interventions:
            self._decision_modules.append(
                PreSpecified(self._timesteps, self.register, planned_interventions)
                )

    def decision_loop(self):
        """Generate bundles of simulation steps to run.

        Each iteration returns a dict: {decision_iteration (int) => list of timesteps (int)}

        With only pre-specified planning, there is a single step in the loop, with a single
        decision iteration with timesteps covering the entire model horizon.

        With a rule based approach, there might be many steps in the loop, each with a single
        decision iteration and single timestep, moving on once some threshold is satisfied.

        With a genetic algorithm, there might be a configurable number of steps in the loop,
        each with multiple decision iterations (one for each member of the algorithm's
        population) and timesteps covering the entire model horizon.

        Implicitly, if the bundle returned in an iteration contains multiple decision
        iterations, they can be performed in parallel. If each decision iteration contains
        multiple timesteps, they can also be parallelised, so long as there are no temporal
        dependencies.
        """
        if len(self._decision_modules) > 0:
            for module in self._decision_modules:
                yield module._get_next_decision_iteration()
        else:
            yield {0: [x for x in self._timesteps]}

    def get_decision(self, data_handle, timestep, iteration):
        """Return all interventions built in the given timestep

        for the given decision
        iteration.

        Arguments
        ---------
        data_handle : smif.data_layer.data_handle.DataHandle
        timestep : int
            A timestep (planning year)
        iteration : int
            A decision iteration

        """
        decisions = []
        for module in self._decision_modules:
            decisions.extend(module.get_decision(data_handle, timestep, iteration))
        self.logger.debug(
            "Retrieved %s decisions from %s",
            len(decisions), str(self._decision_modules))

        self.logger.debug(
            "Writing state for timestep %s and interation %s",
            timestep,
            iteration)
        self._store.write_state(decisions,
                                data_handle._modelrun_name,
                                timestep, iteration)


class DecisionModule(metaclass=ABCMeta):
    """Abstract class which provides the interface to decision mechanisms.

    These mechanisms including Pre-Specified Planning, a Rule-based Approach and
    Multi-objective Optimisation.

    This class provides two main public methods, ``__next__`` which is normally
    called implicitly as a call to the class as an iterator, and ``get_decision()``
    which takes as arguments a smif.model.Model object, and ``timestep`` and
    ``decision_iteration`` integers. The first of these returns a dict of
    decision_iterations and timesteps over which a SosModel should be iterated.
    The latter provides a means to furnish the structure of contained Model
    objects through a list of historical and recent interventions.

    Arguments
    ---------
    timesteps : list
        A list of planning timesteps
    register : dict
        Reference to a dict of iterventions
    """
    def __init__(self, timesteps, register):
        self.timesteps = timesteps
        self.register = register

    def __next__(self):
        return self._get_next_decision_iteration()

    @abstractmethod
    def _get_next_decision_iteration(self):
        """Implement to return the next decision iteration

        Within a list of decision-iteration/timestep pairs, the assumption is
        that all decision iterations can be run in parallel
        (otherwise only one will be returned) and within a decision interation,
        all timesteps may be run in parallel as long as there are no
        inter-timestep state transitions required (e.g. reservoir level)

        Returns
        -------
        dict
            Yields a dictionary keyed by decision iteration (int) whose values contain
            a list of timesteps
        """
        raise NotImplementedError
{'fraction_non_alphanumeric': 0.057439401753481174, 'fraction_numerical': 0.00025786487880350697, 'mean_word_length': 2.8332097850259452, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1183661', 'n_tokens_mistral': 3859, 'n_tokens_neox': 3649, 'n_words': 1520}