"""
@package pmsco.table
table scan optimization handler

the table scan scans through an explicit table of model parameters.
it can be used to recalculate models from a previous optimization run on different scans,
or as an interface to external optimizers.
new elements can be added to the table while the calculation loop is in progress.

though the concepts _population_ and _optimization_ are not intrinsic to a table scan,
the classes defined here inherit from the generic population class and optimization handler.
this is done to share as much code as possible between the different optimizers.
the only difference is that the table optimizer does not generate models internally.
instead, it loads them (possibly repeatedly) from a file or asks the project code to provide the data.

@author Matthias Muntwiler, matthias.muntwiler@psi.ch

@copyright (c) 2015-18 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
  http://www.apache.org/licenses/LICENSE-2.0
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import numpy as np
import pmsco.optimizers.population as population
from pmsco.helpers import BraceMessage as BMsg

logger = logging.getLogger(__name__)


class TablePopulation(population.Population):
    """
    population generated from explicit values.

    this class maintains a population that is updated from a table of explicit values.
    the table can be static (defined at the start of the optimization process)
    or dynamic (new models appended during the optimization process).

    for each generation, the table is read and the next models are imported into the population.
    the class de-duplicates the table, i.e. models with equal parameters as a previous one are not calculated again.
    it is, thus, perfectly fine that new models are appended to the table rather than overwrite previous entries.

    the table can be built from the following data sources:

    @arg (numpy.ndarray): structured array that can be added to self.positions,
        having at least the columns defining the model parameters.
    @arg (sequence of dict, numpy.ndarray, numpy.void, named tuple):
        each element must be syntactically compatible with a dict
        that holds the model parameters.
    @arg (str): file name that contains a table in the same format as
        @ref pmsco.optimizers.population.Population.save_array produces.
    @arg (callable): a function that returns one of the above objects
        (or None to mark the end of the table).

    the data source is passed as an argument to the self.setup() method.
    structured arrays and sequences cannot be modified after they are passed to `setup`.
    this means that the complete table must be known at the start of the process.

    the most flexible way is to pass a function that generates a structured array in each call.
    this would even allow to include a non-standard optimization algorithm.
    the function is best defined in the custom project class.
    the population calls it every time before a new generation starts.
    to end the optimization process, it simply returns None.

    the table can also be defined in an external file, e.g. as calculated by other programs or edited manually.
    the table file can either remain unchanged during the optimization process,
    or new models can be added while the optimization is in progress.
    in the latter case, note that there is no reliable synchronization of file access.

    first, writing to the file must be as short as possible.
    the population class has a read timeout of ten seconds.

    second, because it is impossible to know whether the file has been read or not,
    new models should be _appended_ rather than _overwrite_ previous ones.
    the population class automatically skips models that have already been read.

    this class supports does not support seeding.
    although, a seed file is accepted, it is not used.
    patching is allowed, but there is normally no advantage over modifying the table.

    the model space is used to define the model parameters and the parameter range.
    models violating the parameter model space are ignored.
    """

    ## @var table_source
    # data source of the model table
    #
    # this can be any object accepted by @ref pmsco.optimizers.population.Population.import_positions,
    # e.g. a file name, a numpy structured array, or a function returning a structured array.
    # see the class description for details.

    def __init__(self):
        """
        initialize the population object.

        """
        super(TablePopulation, self).__init__()
        self.table_source = None
        self.position_constrain_mode = 'error'

    def setup(self, size, model_space, **kwargs):
        """
        set up the population arrays, parameter model space and data source.

        @param size: requested number of particles.
            this does not need to correspond to the number of table entries.
            on each generation the population loads up to this number of new entries from the table source.

        @param model_space: definition of initial and limiting model parameters
            expected by the cluster and parameters functions.
            @arg model_space.start: not used.
            @arg model_space.min:   minimum values allowed.
            @arg model_space.max:   maximum values allowed.
            @arg model_space.step:  not used.

        the following arguments are keyword arguments.
        the method also accepts the inherited arguments for seeding. they do not have an effect, however.

        @param table_source: data source of the model table.
            this can be any object accepted by @ref pmsco.optimizers.population.Population.import_positions,
            e.g. a file name, a numpy structured array, or a function returning a structured array.
            see the class description for details.

        @return: None
        """
        super(TablePopulation, self).setup(size, model_space, **kwargs)
        self.table_source = kwargs['table_source']
        self.size_act = 0
        self.model_count = 0

    def advance_population(self):
        """
        advance the population by one step.

        this methods re-imports the table file
        and copies the table to current population.

        @return: None
        """
        self.import_positions(self.table_source)
        self.generation += 1
        self.advance_from_import()
        super(TablePopulation, self).advance_population()


class TableModelHandler(population.PopulationHandler):
    """
    model handler which implements the table algorithm.

    """
    def __init__(self):
        super(TableModelHandler, self).__init__()
        self._pop = TablePopulation()
