2022, Machine Intelligence and Data Science Laboratory - UFMG - Brazil

Machine Intelligence and Data Science Laboratory - UFMG - Brazil

incremental_gaussian() (pyFTS.data.artificial.SignalEmulator method)

periodic_gaussian() (pyFTS.data.artificial.SignalEmulator method)

stationary_gaussian() (pyFTS.data.artificial.SignalEmulator method)

generate_linear_periodic_gaussian() (in module pyFTS.data.artificial)

generate_sinoidal_periodic_gaussian() (in module pyFTS.data.artificial)

generate_high_order_recurrent_flr() (in module pyFTS.common.FLR)

get_maximum_membership_fuzzyset() (in module pyFTS.common.FuzzySet)

get_maximum_membership_fuzzyset_index() (in module pyFTS.common.FuzzySet)

around() (pyFTS.common.SortedCollection.SortedCollection method)

between() (pyFTS.common.SortedCollection.SortedCollection method)

find_ge() (pyFTS.common.SortedCollection.SortedCollection method)

find_gt() (pyFTS.common.SortedCollection.SortedCollection method)

find_le() (pyFTS.common.SortedCollection.SortedCollection method)

find_lt() (pyFTS.common.SortedCollection.SortedCollection method)

insert() (pyFTS.common.SortedCollection.SortedCollection method)

insert_right() (pyFTS.common.SortedCollection.SortedCollection method)

inside() (pyFTS.common.SortedCollection.SortedCollection method)

remove() (pyFTS.common.SortedCollection.SortedCollection method)

parameters (pyFTS.common.transformations.boxcox.BoxCox property)

Differential (class in pyFTS.common.transformations.differential)

apply() (pyFTS.common.transformations.differential.Differential method)

inverse() (pyFTS.common.transformations.differential.Differential method)

parameters (pyFTS.common.transformations.differential.Differential property)

Normalization (class in pyFTS.common.transformations.normalization)

apply() (pyFTS.common.transformations.normalization.Normalization method)

inverse() (pyFTS.common.transformations.normalization.Normalization method)

train() (pyFTS.common.transformations.normalization.Normalization method)

AveragePooling (class in pyFTS.common.transformations.smoothing)

apply() (pyFTS.common.transformations.smoothing.AveragePooling method)

inverse() (pyFTS.common.transformations.smoothing.AveragePooling method)

ExponentialSmoothing (class in pyFTS.common.transformations.smoothing)

apply() (pyFTS.common.transformations.smoothing.ExponentialSmoothing method)

inverse() (pyFTS.common.transformations.smoothing.ExponentialSmoothing method)

apply() (pyFTS.common.transformations.smoothing.MaxPooling method)

inverse() (pyFTS.common.transformations.smoothing.MaxPooling method)

apply() (pyFTS.common.transformations.smoothing.MovingAverage method)

inverse() (pyFTS.common.transformations.smoothing.MovingAverage method)

apply() (pyFTS.common.transformations.som.SOMTransformation method)

save_net() (pyFTS.common.transformations.som.SOMTransformation method)

show_grid() (pyFTS.common.transformations.som.SOMTransformation method)

train() (pyFTS.common.transformations.som.SOMTransformation method)

Transformation (class in pyFTS.common.transformations.transformation)

apply() (pyFTS.common.transformations.transformation.Transformation method)

inverse() (pyFTS.common.transformations.transformation.Transformation method)

MultivariateFuzzySet (class in pyFTS.models.multivariate.common)

append_set() (pyFTS.models.multivariate.common.MultivariateFuzzySet method)

membership() (pyFTS.models.multivariate.common.MultivariateFuzzySet method)

fuzzyfy_instance_clustered() (in module pyFTS.models.multivariate.common)

MultivariatePartitioner (class in pyFTS.models.multivariate.partitioner)

append() (pyFTS.models.multivariate.partitioner.MultivariatePartitioner method)

build() (pyFTS.models.multivariate.partitioner.MultivariatePartitioner method)

fuzzyfy() (pyFTS.models.multivariate.partitioner.MultivariatePartitioner method)

prune() (pyFTS.models.multivariate.partitioner.MultivariatePartitioner method)

search() (pyFTS.models.multivariate.partitioner.MultivariatePartitioner method)

IncrementalGridCluster (class in pyFTS.models.multivariate.grid)

fuzzyfy() (pyFTS.models.multivariate.grid.IncrementalGridCluster method)

prune() (pyFTS.models.multivariate.grid.IncrementalGridCluster method)

get_midpoint() (pyFTS.models.nonstationary.common.FuzzySet method)

location_params (pyFTS.models.nonstationary.common.FuzzySet attribute)

membership() (pyFTS.models.nonstationary.common.FuzzySet method)

noise_params (pyFTS.models.nonstationary.common.FuzzySet attribute)

perform_location() (pyFTS.models.nonstationary.common.FuzzySet method)

perform_width() (pyFTS.models.nonstationary.common.FuzzySet method)

perturbate_parameters() (pyFTS.models.nonstationary.common.FuzzySet method)

width_params (pyFTS.models.nonstationary.common.FuzzySet attribute)

check_bounds_index() (in module pyFTS.models.nonstationary.common)

get_key() (pyFTS.models.nonstationary.flrg.NonStationaryFLRG method)

get_lower() (pyFTS.models.nonstationary.flrg.NonStationaryFLRG method)

get_membership() (pyFTS.models.nonstationary.flrg.NonStationaryFLRG method)

get_midpoint() (pyFTS.models.nonstationary.flrg.NonStationaryFLRG method)

get_upper() (pyFTS.models.nonstationary.flrg.NonStationaryFLRG method)

unpack_args() (pyFTS.models.nonstationary.flrg.NonStationaryFLRG method)

PolynomialNonStationaryPartitioner (class in pyFTS.models.nonstationary.partitioners)

SimpleNonStationaryPartitioner (class in pyFTS.models.nonstationary.partitioners)

simplenonstationary_gridpartitioner_builder() (in module pyFTS.models.nonstationary.partitioners)

exponential() (in module pyFTS.models.nonstationary.perturbation)

polynomial() (in module pyFTS.models.nonstationary.perturbation)

DataFrameSeasonalIndexer (class in pyFTS.models.seasonal.SeasonalIndexer)

DateTimeSeasonalIndexer (class in pyFTS.models.seasonal.SeasonalIndexer)

LinearSeasonalIndexer (class in pyFTS.models.seasonal.SeasonalIndexer)

get_data() (pyFTS.models.seasonal.SeasonalIndexer.LinearSeasonalIndexer method)

SeasonalIndexer (class in pyFTS.models.seasonal.SeasonalIndexer)

get_data() (pyFTS.models.seasonal.SeasonalIndexer.SeasonalIndexer method)

get_index() (pyFTS.models.seasonal.SeasonalIndexer.SeasonalIndexer method)

minute_of_hour (pyFTS.models.seasonal.common.DateTime attribute)

minute_of_month (pyFTS.models.seasonal.common.DateTime attribute)

minute_of_week (pyFTS.models.seasonal.common.DateTime attribute)

minute_of_year (pyFTS.models.seasonal.common.DateTime attribute)

second_of_hour (pyFTS.models.seasonal.common.DateTime attribute)

second_of_minute (pyFTS.models.seasonal.common.DateTime attribute)

TimeGridPartitioner (class in pyFTS.models.seasonal.partitioner)

build() (pyFTS.models.seasonal.partitioner.TimeGridPartitioner method)

build_index() (pyFTS.models.seasonal.partitioner.TimeGridPartitioner method)

extractor() (pyFTS.models.seasonal.partitioner.TimeGridPartitioner method)

mask (pyFTS.models.seasonal.partitioner.TimeGridPartitioner attribute)

plot() (pyFTS.models.seasonal.partitioner.TimeGridPartitioner method)

search() (pyFTS.models.seasonal.partitioner.TimeGridPartitioner method)

season (pyFTS.models.seasonal.partitioner.TimeGridPartitioner attribute)

build_index() (pyFTS.partitioners.partitioner.Partitioner method)

check_bounds() (pyFTS.partitioners.partitioner.Partitioner method)

lower_margin (pyFTS.partitioners.partitioner.Partitioner attribute)

membership_function (pyFTS.partitioners.partitioner.Partitioner attribute)

ordered_sets (pyFTS.partitioners.partitioner.Partitioner attribute)

partitions (pyFTS.partitioners.partitioner.Partitioner attribute)

transformation (pyFTS.partitioners.partitioner.Partitioner attribute)

upper_margin (pyFTS.partitioners.partitioner.Partitioner attribute)

build() (pyFTS.partitioners.ClassPartitioner.ClassPartitioner method)

build() (pyFTS.partitioners.Singleton.SingletonPartitioner method)

append_complex() (pyFTS.partitioners.Simple.SimplePartitioner method)

build() (pyFTS.partitioners.SubClust.SubClustPartitioner method)

f-strings support = for self-documenting expressions and debugging

ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON 3.11.3 DOCUMENTATION

email.message.Message: Representing an email message using the compat32 API

What is the most efficient way to concatenate many strings together?

Why does a_tuple[i] += [‘item’] raise an exception when the addition works?

What does the slash(/) in the parameter list of a function mean?

Why am I getting an UnboundLocalError when the variable has a value?

PEP 3118: New memoryview implementation and buffer protocol documentation

PEP 476: Enabling certificate verification by default for stdlib http clients

PEP 471 - os.scandir() function – a better and faster directory iterator

PEP 655: Marking individual TypedDict items as required or not-required

Why must ‘self’ be used explicitly in method definitions and calls?

argparse — Parser for command-line options, arguments and sub-commands

functools — Higher-order functions and operations on callable objects

multiprocessing.shared_memory — Shared memory for direct access across processes

sysconfig — Provide access to Python’s configuration information

import pandas as pd
import numpy as np
import os
import pkg_resources
from pathlib import Path
from urllib import request

def get_dataframe(filename: str, url: str, sep:str=";", compression:str='infer') -> pd.DataFrame:
    """
    This method check if filename already exists, read the file and return its data.
    If the file don't already exists, it will be downloaded and decompressed.

    :param filename: dataset local filename
    :param url: dataset internet URL
    :param sep: CSV field separator
    :param compression: type of compression
    :return:  Pandas dataset
    """

    tmp_file = Path(filename)

    if tmp_file.is_file():
        return pd.read_csv(filename, sep=sep, compression=compression)
    else:
        request.urlretrieve(url, filename)

"""
Facilities to generate synthetic stochastic processes
"""

class SignalEmulator(object):
    """
    Emulate a complex signal built from several additive and non-additive components
    """

    def __init__(self, **kwargs):
        super(SignalEmulator, self).__init__()

        self.components = []
        """Components of the signal"""

    def stationary_gaussian(self, mu:float, sigma:float, **kwargs):
        """
        Creates a continuous Gaussian signal with mean mu and variance sigma.

        :param mu: mean
        :param sigma: variance
        :keyword additive: If False it cancels the previous signal and start this one, if True
                           this signal is added to the previous one
        :keyword start: lag index to start this signal, the default value is 0
        :keyword it: Number of iterations, the default value is 1
        :keyword length: Number of samples generated on each iteration, the default value is 100
        :keyword vmin: Lower bound value of generated data, the default value is None
        :keyword vmax: Upper bound value of generated data, the default value is None
        :return: the current SignalEmulator instance, for method chaining
        """
        parameters = {'mu': mu, 'sigma': sigma}
        self.components.append({'dist': 'gaussian', 'type': 'constant',
                                'parameters': parameters, 'args': kwargs})
        return self

    def incremental_gaussian(self, mu:float, sigma:float, **kwargs):
        """
        Creates an additive gaussian interference on a previous signal

        :param mu: increment on mean
        :param sigma: increment on variance
        """
        self.components.append({'dist': 'gaussian', 'type': 'incremental',

    def periodic_gaussian(self, type:str, period:int, mu_min:float, sigma_min:float, mu_max:float, sigma_max:float, **kwargs):
        """
        Creates an additive periodic gaussian interference on a previous signal

        :param type: 'linear' or 'sinoidal'
        :param period: the period of recurrence
        """
        parameters = {'type':type, 'period':period,
                      'mu_min': mu_min, 'sigma_min': sigma_min, 'mu_max': mu_max, 'sigma_max': sigma_max}
        self.components.append({'dist': 'gaussian', 'type': 'periodic',

    def blip(self, **kwargs):
        """
        Creates an outlier greater than the maximum or lower then the minimum previous values of the signal,
        and insert it on a random location of the signal.

        """
        parameters = {}
        self.components.append({'dist': 'blip', 'type': 'blip',
                                'parameters': parameters, 'args':kwargs})

    def run(self):
        """
        Render the signal

        :return: a list of float values
        """
        signal = []
        last_it = 10
        last_num = 10
        for ct, component in enumerate(self.components):
            parameters = component['parameters']
            kwargs = component['args']
            additive = kwargs.get('additive', True)
            start = kwargs.get('start', 0)
            it = kwargs.get('it', last_it)
            num = kwargs.get('length', last_num)
            vmin = kwargs.get('vmin',None)
            vmax = kwargs.get('vmax', None)
            if component['type'] == 'constant':
                tmp = generate_gaussian_linear(parameters['mu'], parameters['sigma'], 0, 0,
                                         it=it, num=num, vmin=vmin, vmax=vmax)
            elif component['type'] == 'incremental':
                tmp = generate_gaussian_linear(0, 0, parameters['mu'], parameters['sigma'],
                                         it=num, num=1, vmin=vmin, vmax=vmax)
            elif component['type'] == 'periodic':
                period = parameters['period']
                mu_min, sigma_min = parameters['mu_min'],parameters['sigma_min']
                mu_max, sigma_max = parameters['mu_max'],parameters['sigma_max']

                if parameters['type'] == 'sinoidal':
                    tmp = generate_sinoidal_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max,
                                                              it=num, num=1, vmin=vmin, vmax=vmax)
                else:
                    tmp = generate_linear_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max,
                                                        it=num, num=1, vmin=vmin, vmax=vmax)
            elif component['type'] == 'blip':
                _mx = np.nanmax(signal)
                _mn = np.nanmin(signal)

                _mx += 2*_mx if _mx > 0 else -2*_mx
                _mn += -2*_mn if _mn > 0 else 2*_mn

                if vmax is not None:
                    _mx = min(_mx, vmax) if vmax > 0 else max(_mx, vmax)
                if vmin is not None:
                    _mn = max(_mn, vmin) if vmin > 0 else min(_mn, vmin)

                start = np.random.randint(0, len(signal))
                tmp = [_mx] if np.random.rand() >= .5 else [-_mn]

            last_num = num
            last_it = it

            signal = _append(additive, start, signal, tmp)

        return signal

def generate_gaussian_linear(mu_ini, sigma_ini, mu_inc, sigma_inc, it=100, num=10, vmin=None, vmax=None):
    """
    Generate data sampled from Gaussian distribution, with constant or linear changing parameters

    :param mu_ini: Initial mean
    :param sigma_ini: Initial variance
    :param mu_inc:  Mean increment after 'num' samples
    :param sigma_inc: Variance increment after 'num' samples
    :param it: Number of iterations
    :param num: Number of samples generated on each iteration
    :param vmin: Lower bound value of generated data
    :param vmax: Upper bound value of generated data
    :return: A list of it*num float values
    """
    mu = mu_ini
    sigma = sigma_ini
    ret = []
    for k in np.arange(0,it):
        tmp = np.random.normal(mu, sigma, num)
        if vmin is not None:
            tmp = np.maximum(np.full(num, vmin), tmp)
        if vmax is not None:
            tmp = np.minimum(np.full(num, vmax), tmp)
        ret.extend(tmp)
        mu += mu_inc
        sigma += sigma_inc
    return ret

def generate_linear_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max, it=100, num=10, vmin=None, vmax=None):
    """
    Generates a periodic linear variation on mean and variance

    :param period: the period of recurrence
    :param mu_min: initial (and minimum) mean of each period
    :param sigma_min: initial (and minimum) variance of each period
    :param mu_max: final (and maximum) mean of each period
    :param sigma_max: final (and maximum) variance of each period
    """

    if period > it:
        raise("The 'period' parameter must be lesser than 'it' parameter")

    mu_inc = (mu_max - mu_min)/period
    sigma_inc = (sigma_max - sigma_min) / period
    mu = mu_min
    sigma = sigma_min
    ret = []
    signal = True

    for k in np.arange(0, it):

        if k % period == 0:
            signal = not signal

        mu += (mu_inc if signal else -mu_inc)
        sigma += (sigma_inc if signal else -sigma_inc)

        sigma = max(sigma, 0.005)

    return ret

def generate_sinoidal_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max, it=100, num=10, vmin=None, vmax=None):
    """
    Generates a periodic sinoidal variation on mean and variance

    """
    mu_range = mu_max - mu_min
    sigma_range = sigma_max - sigma_min
    mu = mu_min
    ret = []

        mu += mu_range * np.sin(period * k)
        sigma += sigma_range * np.sin(period * k)

    return ret

def generate_uniform_linear(min_ini, max_ini, min_inc, max_inc, it=100, num=10, vmin=None, vmax=None):
    """
    Generate data sampled from Uniform distribution, with constant or  linear changing bounds

    """
    _min = min_ini
    _max = max_ini
    ret = []
        tmp = np.random.uniform(_min, _max, num)
        _min += min_inc
        _max += max_inc
    return ret

def white_noise(n=500):
    """
    Simple Gaussian noise signal
    :param n: number of samples
    :return:
    """
    return np.random.normal(0, 1, n)

def random_walk(n=500, type='gaussian'):
    """
    Simple random walk

    :param type: 'gaussian' or 'uniform'
    :return:
    """
    if type == 'gaussian':
        tmp = generate_gaussian_linear(0, 1, 0, 0, it=1, num=n)
    else:
        tmp = generate_uniform_linear(-1, 1, 0, 0, it=1, num=n)
    ret = [0]
    for i in range(n):
        ret.append(tmp[i] + ret[i])

    return ret

def _append(additive, start, before, new):
    if not additive:
        before.extend(new)
        return before
    else:
        for k in range(start):
            new.insert(0,0)

        l1 = len(before)
        l2 = len(new)

        if l2 < l1:
            new.extend(np.zeros(l1 - l2).tolist())
        elif 0 < l1 < l2:
            new = new[:l1]

        if len(before) == 0:
            tmp = np.array(new)
        else:
            tmp = np.array(before) + np.array(new)
        return tmp.tolist()

"""
Monthly totals of a airline passengers from USA, from January 1949 through December 1960.

Source: Hyndman, R.J., Time Series Data Library, http://www-personal.buseco.monash.edu.au/~hyndman/TSDL/.
"""

from pyFTS.data import common

def get_data() -> np.ndarray:
    """
    Get a simple univariate time series data.

    :return: numpy array
    """
    dat = get_dataframe()
    dat = np.array(dat["Passengers"])
    return dat

def get_dataframe() -> pd.DataFrame:
    """
    Get the complete multivariate time series data.

    :return: Pandas DataFrame
    """
    dat = common.get_dataframe('AirPassengers.csv',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/AirPassengers.csv',
                               sep=",")
    return dat

"""
Bitcoin to USD quotations

Daily averaged index, by business day, from 2010 to 2018.

Source: https://finance.yahoo.com/quote/BTC-USD?p=BTC-USD
"""

def get_data(field:str='AVG') -> np.ndarray:
    """
    Get the univariate time series data.

    :param field: dataset field to load
    """
    return np.array(dat[field])

    """

    """
    df = common.get_dataframe("BTCUSD.csv", "https://query.data.world/s/72gews5w3c7oaf7by5vp7evsasluia",
                              sep=",")

    return df

"""
DJI - Dow Jones

Daily averaged index, by business day, from 1985 to 2017.

Source: https://finance.yahoo.com/quote/%5EGSPC/history?p=%5EGSPC
"""

    """

    """

    """

    """
    df = common.get_dataframe("DowJones.csv", "https://query.data.world/s/d4hfir3xrelkx33o3bfs5dbhyiztml",
                              sep=",")

    return df

"""
Yearly University of Alabama enrollments from 1971 to 1992.
"""

    """

    """
    dat = np.array(dat["Enrollments"])
    return dat

    dat = common.get_dataframe('Enrollments.csv',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/Enrollments.csv',
                               sep=";")
    return dat

"""
Ethereum to USD quotations

Daily averaged index, by business day, from 2016 to 2018.

Source: https://finance.yahoo.com/quote/ETH-USD?p=ETH-USD
"""

def get_data(field: str='AVG') -> np.ndarray:
    """

    """

    """

    """
    df = common.get_dataframe("ETHUSD.csv", "https://query.data.world/s/qj4ly7o4rl7oq527xzy4v76wkr3hws",
                              sep=",")

    return df

"""
FOREX market EUR-GBP pair.

Daily averaged quotations, by business day, from 2016 to 2018.
"""

def get_data(field:str='avg') -> np.ndarray:
    """

    """

    """

    """
    df = common.get_dataframe("EURGBP.csv", "https://query.data.world/s/gvsaeruthnxjkwzl7z4ki7u5rduah3",
                              sep=",")

    return df

"""
FOREX market EUR-USD pair.

"""

def get_data(field: str='avg') -> np.ndarray:
    """

    """

    """

    """
    df = common.get_dataframe("EURUSD.csv", "https://query.data.world/s/od4eojioz4w6o5bbwxjfn6j5zoqtos",
                              sep=",")

    return df

"""
FOREX market GBP-USD pair.

"""

    """

    """

    """

    """
    df = common.get_dataframe("GBPUSD.csv", "https://query.data.world/s/sw4mijpowb3mqv6bsat7cdj54hyxix",
                              sep=",")

    return df

"""
INMET - Instituto Nacional Meteorologia / Brasil

Belo Horizonte station, from 2000-01-01 to  31/12/2012

Source: http://www.inmet.gov.br
"""

    """

    """
    dat = common.get_dataframe('INMET.csv.bz2',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/INMET.csv.bz2',
                               sep=";", compression='bz2')
    dat["DataHora"] = pd.to_datetime(dat["DataHora"], format='%d/%m/%Y %H:%M')
    return dat

"""
Hourly Malaysia eletric load and tempeature
"""

def get_data(field: str='load') -> np.ndarray:
    """

    """

    """

    """
    df = common.get_dataframe("malaysia.csv","https://query.data.world/s/e5arbthdytod3m7wfcg7gmtluh3wa5",
                               sep=";")

    return df

"""
National Association of Securities Dealers Automated Quotations - Composite Index (NASDAQ IXIC)

Daily averaged index by business day, from 2000 to 2016.

Source: http://www.nasdaq.com/aspx/flashquotes.aspx?symbol=IXIC&selected=IXIC
"""

def get_data(field: str="avg") -> np.ndarray:
    """

    :param field: the dataset field name to extract
    """
    dat = np.array(dat[field])
    return dat

    """

    """
    dat = common.get_dataframe('NASDAQ.csv.bz2',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/NASDAQ.csv.bz2',
                               sep=",", compression='bz2')
    return dat

"""
SONDA - Sistema de Organização Nacional de Dados Ambientais, from INPE - Instituto Nacional de Pesquisas Espaciais, Brasil.

Brasilia station

Source: http://sonda.ccst.inpe.br/

"""

def get_data(field:str) -> np.ndarray:
    """

    """
    return dat

    """

    """
    dat = common.get_dataframe('SONDA_BSB.csv.bz2',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/SONDA_BSB.csv.bz2',
    dat["datahora"] = pd.to_datetime(dat["datahora"], format='%Y-%m-%d %H:%M:%S')
    return dat

"""
S&P500 - Standard & Poor's 500

Daily averaged index, by business day, from 1950 to 2017.

"""

    """

    """
    return np.array(dat["Avg"])

    """

    """
    dat = common.get_dataframe('SP500.csv.bz2',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/SP500.csv.bz2',
    return dat

"""
The Taiwan Stock Exchange Capitalization Weighted Stock Index (TAIEX)

Daily averaged index by business day, from 1995 to 2014.

Source: http://www.twse.com.tw/en/products/indices/Index_Series.php
"""

    """

    """
    dat = np.array(dat["avg"])
    return dat

    """

    """
    dat = common.get_dataframe('TAIEX.csv.bz2',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/TAIEX.csv.bz2',
    dat["Date"] = pd.to_datetime(dat["Date"])
    return dat

"""
M. Hénon. "A two-dimensional mapping with a strange attractor". Commun. Math. Phys. 50, 69-77 (1976)

dx/dt = a + by(t-1) - x(t-1)^2
dy/dt = x
"""

def get_data(var: str, a:float=1.4, b:float=0.3, initial_values: list = [1, 1], iterations:int=1000) -> pd.DataFrame:
    """

    :param var: the dataset field name to extract
    """
    return get_dataframe(a,b, initial_values, iterations)[var].values

def get_dataframe(a:float=1.4, b:float=0.3, initial_values: list = [1, 1], iterations:int=1000) -> pd.DataFrame:
    '''
    Return a dataframe with the bivariate Henon Map time series (x, y).

    :param a: Equation coefficient
    :param b: Equation coefficient
    :param initial_values: numpy array with the initial values of x and y. Default: [1, 1]
    :param iterations: number of iterations. Default: 1000
    :return: Panda dataframe with the x and y values
    '''

    x = [initial_values[0]]
    y = [initial_values[1]]
    for t in np.arange(0, iterations):
        xx = a + b * y[t] - x[t] ** 2
        y.append(x[t])
        x.append(xx)

    return pd.DataFrame({'x': x, 'y':y})

"""
May, Robert M. (1976). "Simple mathematical models with very complicated dynamics".
Nature. 261 (5560): 459–467. doi:10.1038/261459a0.

x(t) = r * x(t-1) * (1 - x(t -1) )
"""

def get_data(r: float = 4, initial_value: float = 0.3, iterations: int=100) -> list:
    '''
    Return a list with the logistic map chaotic time series.

    :param r: Equation coefficient
    :param initial_value: Initial value of x. Default: 0.3
    :param iterations: number of iterations. Default: 100
    :return:
    '''
    x = [initial_value]
    for t in np.arange(0,iterations):
      x.append(r * x[t]*(1 - x[t]))

    return x

"""
Lorenz, Edward Norton (1963). "Deterministic nonperiodic flow". Journal of the Atmospheric Sciences. 20 (2): 130–141.
https://doi.org/10.1175/1520-0469(1963)020<0130:DNF>2.0.CO;2

dx/dt = a(y -x)
dy/dt = x(b - z) - y
dz/dt = xy - cz
"""

def get_data(var: str, a: float = 10.0, b: float = 28.0, c: float = 8.0 / 3.0, dt: float = 0.01,
                  initial_values: list = [0.1, 0, 0], iterations: int=1000) -> pd.DataFrame:
    """
        Get a simple univariate time series data.

        :param var: the dataset field name to extract
        :return: numpy array
        """
    return get_dataframe(a, b, c, dt, initial_values, iterations)[var].values

def get_dataframe(a: float = 10.0, b: float = 28.0, c: float = 8.0 / 3.0, dt: float = 0.01,
                  initial_values: list = [0.1, 0, 0], iterations: int=1000)-> pd.DataFrame:
    '''
    Return a dataframe with the multivariate Lorenz Map time series (x, y, z).

    :param a: Equation coefficient. Default value: 10
    :param b: Equation coefficient. Default value: 28
    :param c: Equation coefficient. Default value: 8.0/3.0
    :param dt: Time differential for continuous time integration. Default value: 0.01
    :param initial_values: numpy array with the initial values of x,y and z. Default: [0.1, 0, 0]
    :return: Panda dataframe with the x, y and z values
    '''

    z = [initial_values[2]]

        dxdt = a * (y[t] - x[t])
        dydt = x[t] * (b - z[t]) - y[t]
        dzdt = x[t] * y[t] - c * z[t]
        x.append(x[t] + dt * dxdt)
        y.append(y[t] + dt * dydt)
        z.append(z[t] + dt * dzdt)

    return pd.DataFrame({'x': x, 'y':y, 'z': z})

"""
Mackey, M. C. and Glass, L. (1977). Oscillation and chaos in physiological control systems.
Science, 197(4300):287-289.

dy/dt = -by(t)+ cy(t - tau) / 1+y(t-tau)^10
"""

def get_data(b: float=0.1, c: float=0.2, tau: float =17, initial_values: np.ndarray = np.linspace(0.5,1.5, 18), iterations: int=1000) -> list:
    '''
    Return a list with the Mackey-Glass chaotic time series.

    :param c: Equation coefficient
    :param tau: Lag parameter, default: 17
    :param initial_values: numpy array with the initial values of y. Default: np.linspace(0.5,1.5,18)
    :return:
    '''
    y = initial_values.tolist()

    for n in np.arange(len(y)-1, iterations+100):
        y.append(y[n] - b * y[n] + c * y[n - tau] / (1 + y[n - tau] ** 10))

    return y[100:]

"""
O. E. Rössler, Phys. Lett. 57A, 397 (1976).

dx/dt = -z - y
dy/dt = x + ay
dz/dt = b + z( x - c )

"""

def get_data(var: str, a: float = 0.2, b: float = 0.2, c: float = 5.7, dt: float = 0.01,
                  initial_values: np.ndarray = [0.001, 0.001, 0.001], iterations: int=5000) -> np.ndarray:
    """

    """

def get_dataframe(a: float = 0.2, b: float = 0.2, c: float = 5.7, dt: float = 0.01,
                  initial_values: np.ndarray = [0.001, 0.001, 0.001], iterations: int=5000) -> pd.DataFrame:
    '''
    Return a dataframe with the multivariate Rössler Map time series (x, y, z).

    :param a: Equation coefficient. Default value: 0.2
    :param b: Equation coefficient. Default value: 0.2
    :param c: Equation coefficient. Default value: 5.7
    :param initial_values: numpy array with the initial values of x,y and z. Default: [0.001, 0.001, 0.001]
    :param iterations: number of iterations. Default: 5000
    '''

        dxdt = - (y[t] + z[t])
        dydt = x[t] + a * y[t]
        dzdt = b + z[t] * x[t] - z[t] * c

"""
Monthly sunspot numbers from 1749 to May 2016

Source: https://www.esrl.noaa.gov/psd/gcos_wgsp/Timeseries/SUNSPOT/
"""

    """

    """
    dat = np.array(dat["SUNACTIVITY"])
    return dat

    """

    """
    dat = common.get_dataframe('sunspots.csv',
                               'https://github.com/petroniocandido/pyFTS/raw/8f20f3634aa6a8f58083bdcd1bbf93795e6ed767/pyFTS/data/sunspots.csv',
                               sep=",")
    return dat

"""
Activation functions for Time Series Classification
"""

import math
from pyFTS import *

def scale(dist : dict) -> dict:
  norm = np.sum([v for v in dist.values()])
  return {k : (v / norm) for k,v in dist.items() }

def softmax(dist : dict) -> dict:
  norm = np.sum([np.exp(v) for v in dist.values()])
  return {k : (np.exp(v) / norm) for k,v in dist.items() }

def argmax(dist : dict) -> str:
  mx = np.max([v for v in dist.values()])
  return [k for k,v in dist.items() if v == mx ][0]

"""
Composite Fuzzy Sets
"""

from pyFTS.common import Membership, FuzzySet

class FuzzySet(FuzzySet.FuzzySet):
    """
    Composite Fuzzy Set
    """
    def __init__(self, name, superset=False, **kwargs):
        """
        Create an empty composite fuzzy set
        :param name: fuzzy set name
        """
        if 'type' in kwargs:
            kwargs.pop('type')
        super(FuzzySet, self).__init__(name, None, None, None, type='composite', **kwargs)
        self.superset = superset
        if self.superset:
            self.sets = []
        else:
            self.mf = []
            self.parameters = []

        self.lower = None
        self.upper = None
        self.centroid = None

    def membership(self, x):
        """
        Calculate the membership value of a given input

        :param x: input value
        :return: membership value of x at this fuzzy set
        """
            return max([s.membership(x) for s in self.sets])
        else:
            return min([self.mf[ct](self.transform(x), self.parameters[ct]) for ct in np.arange(0, len(self.mf))])

    def transform(self, x):
        return self.sets[0].transform(x)

    def append(self, mf, parameters):
        """
        Adds a new function to composition

        :param mf:
        :param parameters:
        :return:
        """
        self.mf.append(mf)
        self.parameters.append(parameters)

    def append_set(self, set):
        """

        :param mf:
        :return:
        """
        self.sets.append(set)

        if self.lower is None or self.lower > set.lower:
            self.lower = set.lower

        if self.upper is None or self.upper < set.upper:
            self.upper = set.upper

        if self.centroid is None or self.centroid < set.centroid:
            self.centroid = set.centroid

    def __str__(self):
        tmp = str([str(k) for k in self.sets])
        return "{}: {}".format(self.name, tmp)

"""
This module implements functions for Fuzzy Logical Relationship generation
"""

from pyFTS.common import FuzzySet

class FLR(object):
    """
    Fuzzy Logical Relationship

    Represents a temporal transition of the fuzzy set LHS on time t for the fuzzy set RHS on time t+1.
    """
    def __init__(self, LHS, RHS):
        """
        Creates a Fuzzy Logical Relationship
        """
        self.LHS = LHS
        """Left Hand Side fuzzy set"""
        self.RHS = RHS
        """Right Hand Side fuzzy set"""

        return str(self.LHS) + " -> " + str(self.RHS)

class IndexedFLR(FLR):
    """Season Indexed Fuzzy Logical Relationship"""
    def __init__(self, index, LHS, RHS):
        """
        Create a Season Indexed Fuzzy Logical Relationship
        """
        super(IndexedFLR, self).__init__(LHS, RHS)
        self.index = index
        """seasonal index"""

        return str(self.index) + ": "+ str(self.LHS) + " -> " + str(self.RHS)

def generate_high_order_recurrent_flr(fuzzyData):
    """
    Create a ordered FLR set from a list of fuzzy sets with recurrence

    :param fuzzyData: ordered list of fuzzy sets
    :return: ordered list of FLR
    """
    flrs = []
    for i in np.arange(1,len(fuzzyData)):
        lhs = fuzzyData[i - 1]
        rhs = fuzzyData[i]
        if isinstance(lhs, list) and isinstance(rhs, list):
            for l in lhs:
                for r in rhs:
                    tmp = FLR(l, r)
                    flrs.append(tmp)
        else:
            tmp = FLR(lhs,rhs)
            flrs.append(tmp)
    return flrs

def generate_recurrent_flrs(fuzzyData, steps = 1):
    """

    :param steps: the number of steps ahead on the right side of FLR
    """
    _tmp_steps = steps - 1
    flrs = []
    for i in np.arange(1,len(fuzzyData) - _tmp_steps):
        lhs = [fuzzyData[i - 1]]
        rhs = [fuzzyData[i+_tmp_steps]]
        for l in np.array(lhs).flatten():
            for r in np.array(rhs).flatten():
                tmp = FLR(l, r)
                flrs.append(tmp)

def generate_non_recurrent_flrs(fuzzyData, steps = 1):
    """
    Create a ordered FLR set from a list of fuzzy sets without recurrence

    """
    flrs = generate_recurrent_flrs(fuzzyData, steps=steps)
    tmp = {}
    for flr in flrs: tmp[str(flr)] = flr
    ret = [value for key, value in tmp.items()]
    return ret

def generate_indexed_flrs(sets, indexer, data, transformation=None, alpha_cut=0.0):
    """
    Create a season-indexed ordered FLR set from a list of fuzzy sets with recurrence

    :param sets: fuzzy sets
    :param indexer: seasonality indexer 
    :param data: original data
    :return: ordered list of FLR 
    """
    flrs = []
    index = indexer.get_season_of_data(data)
    ndata = indexer.get_data(data)
    if transformation is not None:
        ndata = transformation.apply(ndata)
    for k in np.arange(1,len(ndata)):
        lhs = FuzzySet.fuzzyfy_series([ndata[k - 1]], sets, method='fuzzy',alpha_cut=alpha_cut)
        rhs = FuzzySet.fuzzyfy_series([ndata[k]], sets, method='fuzzy',alpha_cut=alpha_cut)
        season = index[k]
        for _l in np.array(lhs).flatten():
            for _r in np.array(rhs).flatten():
                flr = IndexedFLR(season,_l,_r)
                flrs.append(flr)

from pyFTS.common import Membership

class FuzzySet(object):
    """
    Fuzzy Set
    """

    def __init__(self, name : str, mf, parameters : list, centroid : float, alpha : float=1.0, **kwargs):
        """
        Create a Fuzzy Set
        """
        self.name : str = name
        """The fuzzy set name"""
        self.mf  = mf
        """The membership function"""
        self.parameters : list = parameters
        """The parameters of the membership function"""
        self.centroid : float = centroid
        """The fuzzy set center of mass (or midpoint)"""
        self.alpha : float = alpha
        """The alpha cut value"""
        self.type : str = kwargs.get('type', 'common')
        """The fuzzy set type (common, composite, nonstationary, etc)"""
        self.variable = kwargs.get('variable', None)
        """In multivariate time series, indicate for which variable this fuzzy set belogs"""
        self.Z = None
        """Partition function in respect to the membership function"""

        if parameters is not None:
            if self.mf == Membership.gaussmf:
                self.lower = parameters[0] - parameters[1] * 3
                self.upper = parameters[0] + parameters[1] * 3
            elif self.mf == Membership.sigmf:
                k = (parameters[1] / (2 * parameters[0]))
                self.lower = parameters[1] - k
                self.upper = parameters[1] + k
            else:
                self.lower = min(parameters)
                self.upper = max(parameters)

        self.metadata = {}

        """
        Preprocess the data point for non native types

        :param x:
        :return: return a native type value for the structured type
        """

        return x

        """

        :param x: input value 
        """
        return self.mf(self.transform(x), self.parameters) * self.alpha

    def partition_function(self, uod=None, nbins=100):
        """
        Calculate the partition function over the membership function.

        :param uod:
        :param nbins:
        :return:
        """
        if self.Z is None and uod is not None:
            self.Z = 0.0
            for k in np.linspace(uod[0], uod[1], nbins):
                self.Z += self.membership(k)

        return self.Z

        return self.name + ": " + str(self.mf.__name__) + "(" + str(self.parameters) + ")"

def __binary_search(x, fuzzy_sets, ordered_sets):
    """
    Search for elegible fuzzy sets to fuzzyfy x

    :param x: input value to be fuzzyfied
    :param fuzzy_sets:  a dictionary where the key is the fuzzy set name and the value is the fuzzy set object.
    :param ordered_sets: a list with the fuzzy sets names ordered by their centroids.
    :return: A list with the best fuzzy sets that may contain x
    """
    max_len = len(fuzzy_sets) - 1
    first = 0
    last = max_len

    while first <= last:
        midpoint = (first + last) // 2

        fs = ordered_sets[midpoint]
        fs1 = ordered_sets[midpoint - 1] if midpoint > 0 else ordered_sets[0]
        fs2 = ordered_sets[midpoint + 1] if midpoint < max_len else ordered_sets[max_len]

        if fuzzy_sets[fs1].centroid <= fuzzy_sets[fs].transform(x) <= fuzzy_sets[fs2].centroid:
            return (midpoint - 1, midpoint, midpoint + 1)
        elif midpoint <= 1:
            return [0]
        elif midpoint >= max_len:
            return [max_len]
        else:
            if fuzzy_sets[fs].transform(x) < fuzzy_sets[fs].centroid:
                last = midpoint - 1
            else:
                first = midpoint + 1

def fuzzyfy(data, partitioner, **kwargs):
    """
    A general method for fuzzyfication.

    :param data: input value to be fuzzyfied
    :param partitioner: a trained pyFTS.partitioners.Partitioner object
    :param kwargs: dict, optional arguments
    :keyword alpha_cut: the minimal membership value to be considered on fuzzyfication (only for mode='sets')
    :keyword method: the fuzzyfication method (fuzzy: all fuzzy memberships, maximum: only the maximum membership)
    :keyword mode: the fuzzyfication mode (sets: return the fuzzy sets names, vector: return a vector with the membership
    values for all fuzzy sets, both: return a list with tuples (fuzzy set, membership value) )
    
    :returns a list with the fuzzyfied values, depending on the mode

    """
    alpha_cut = kwargs.get('alpha_cut', 0.)
    mode = kwargs.get('mode', 'sets')
    method = kwargs.get('method', 'fuzzy')
    if isinstance(data, (list, np.ndarray)):
        if mode == 'vector':
            return fuzzyfy_instances(data, partitioner.sets, partitioner.ordered_sets)
        elif mode == 'both':
            mvs = fuzzyfy_instances(data, partitioner.sets, partitioner.ordered_sets)
            fs = []
            for mv in mvs:
                fsets = [(partitioner.ordered_sets[ix], mv[ix])
                         for ix in np.arange(len(mv))
                         if mv[ix] >= alpha_cut]
                fs.append(fsets)
            return fs
        else:
            return fuzzyfy_series(data, partitioner.sets, method, alpha_cut, partitioner.ordered_sets)
    else:
            return fuzzyfy_instance(data, partitioner.sets, partitioner.ordered_sets)
            mv = fuzzyfy_instance(data, partitioner.sets, partitioner.ordered_sets)
            fsets = [(partitioner.ordered_sets[ix], mv[ix])
                     for ix in np.arange(len(mv))
                     if mv[ix] >= alpha_cut]
            return fsets
        else:
            return get_fuzzysets(data, partitioner.sets, partitioner.ordered_sets, alpha_cut)

def set_ordered(fuzzy_sets):
    """
    Order a fuzzy set list by their centroids

    :param fuzzy_sets: a dictionary where the key is the fuzzy set name and the value is the fuzzy set object.
    :return: a list with the fuzzy sets names ordered by their centroids.
    """
    if len(fuzzy_sets) > 0:
        tmp1 = [fuzzy_sets[k] for k in fuzzy_sets.keys()]
        return [k.name for k in sorted(tmp1, key=lambda x: x.centroid)]

def fuzzyfy_instance(inst, fuzzy_sets : dict , ordered_sets : list =None):
    """
    Calculate the membership values for a data point given fuzzy sets

    :param inst: data point
    :return: array of membership values
    """

    if ordered_sets is None:
        ordered_sets = set_ordered(fuzzy_sets)

    mv = np.zeros(len(fuzzy_sets))

    for ix in __binary_search(inst, fuzzy_sets, ordered_sets):
        mv[ix] = fuzzy_sets[ordered_sets[ix]].membership(inst)

    return mv

def fuzzyfy_instances(data : list, fuzzy_sets : dict, ordered_sets=None) -> list:
    """

    """
    ret = []
    for inst in data:
        mv = fuzzyfy_instance(inst, fuzzy_sets, ordered_sets)
        ret.append(mv)
    return ret

def get_fuzzysets(inst, fuzzy_sets : dict, ordered_sets : list =None, alpha_cut : float =0.0) -> list:
    """
    Return the fuzzy sets which membership value for a inst is greater than the alpha_cut

    :param alpha_cut: Minimal membership to be considered on fuzzyfication process
    """

    try:
        fs = [ordered_sets[ix]
              for ix in __binary_search(inst, fuzzy_sets, ordered_sets)
              if fuzzy_sets[ordered_sets[ix]].membership(inst) > alpha_cut]
        return fs
    except Exception as ex:
        raise ex

def get_maximum_membership_fuzzyset(inst, fuzzy_sets, ordered_sets=None) -> FuzzySet:
    """
    Fuzzify a data point, returning the fuzzy set with maximum membership value

    :return: fuzzy set with maximum membership
    """
    mv = np.array([fuzzy_sets[key].membership(inst) for key in ordered_sets])
    key = ordered_sets[np.argwhere(mv == max(mv))[0, 0]]
    return fuzzy_sets[key]

def get_maximum_membership_fuzzyset_index(inst, fuzzy_sets) -> int:
    """

    :param fuzzy_sets: dict of fuzzy sets
    """
    mv = fuzzyfy_instance(inst, fuzzy_sets)
    return np.argwhere(mv == max(mv))[0, 0]

def fuzzyfy_series_old(data, fuzzy_sets, method='maximum'):
    fts = []
    for item in data:
        fts.append(get_maximum_membership_fuzzyset(item, fuzzy_sets).name)
    return fts

def fuzzyfy_series(data, fuzzy_sets, method='maximum', alpha_cut=0.0, ordered_sets=None):
    fts = []
    for t, i in enumerate(data):
        mv = fuzzyfy_instance(i, fuzzy_sets, ordered_sets)
        if len(mv) == 0:
            sets = check_bounds(i, fuzzy_sets.items(), ordered_sets)
        else:
            if method == 'fuzzy':
                ix = np.ravel(np.argwhere(mv > alpha_cut))
                sets = [fuzzy_sets[ordered_sets[i]].name for i in ix]
            elif method == 'maximum':
                mx = max(mv)
                ix = np.ravel(np.argwhere(mv == mx))
                sets = fuzzy_sets[ordered_sets[ix[0]]].name
        fts.append(sets)
    return fts

def grant_bounds(data, fuzzy_sets, ordered_sets):
    if data < fuzzy_sets[ordered_sets[0]].lower:
        return fuzzy_sets[ordered_sets[0]].lower
    elif data > fuzzy_sets[ordered_sets[-1]].upper:
        return fuzzy_sets[ordered_sets[-1]].upper
    else:
        return data

def check_bounds(data, fuzzy_sets, ordered_sets):
        return fuzzy_sets[ordered_sets[0]]
        return fuzzy_sets[ordered_sets[-1]]

def check_bounds_index(data, fuzzy_sets, ordered_sets):
    if data < fuzzy_sets[ordered_sets[0]].get_lower():
        return 0
    elif data > fuzzy_sets[ordered_sets[-1]].get_upper():
        return len(fuzzy_sets) - 1

"""
Membership functions for Fuzzy Sets
"""

def trimf(x, parameters):
    """
    Triangular fuzzy membership function

    :param x: data point
    :param parameters: a list with 3 real values
    :return: the membership value of x given the parameters
    """

    xx = np.round(x, 3)
    if xx < parameters[0]:
        return 0
    elif parameters[0] <= xx < parameters[1]:
        return (x - parameters[0]) / (parameters[1] - parameters[0])
    elif parameters[1] <= xx <= parameters[2]:
        return (parameters[2] - xx) / (parameters[2] - parameters[1])
    else:
        return 0

def trapmf(x, parameters):
    """
    Trapezoidal fuzzy membership function

    :param parameters: a list with 4 real values
    """

    if x < parameters[0]:
        return 0
    elif parameters[0] <= x < parameters[1]:
    elif parameters[1] <= x <= parameters[2]:
        return 1
    elif parameters[2] <= x <= parameters[3]:
        return (parameters[3] - x) / (parameters[3] - parameters[2])
    else:
        return 0

def gaussmf(x, parameters):
    """
    Gaussian fuzzy membership function

    :param parameters: a list with 2 real values (mean and variance)
    """

    return math.exp((-(x - parameters[0])**2)/(2 * parameters[1]**2))

def bellmf(x, parameters):
    """
    Bell shaped membership function

    :param x:
    :param parameters:
    :return:
    """

    return 1 / (1 + abs((x - parameters[2]) / parameters[0]) ** (2 * parameters[1]))

def sigmf(x, parameters):
    """
    Sigmoid / Logistic membership function

    :param x:
    :param parameters: an list with 2 real values (smoothness and midpoint)
    :return
    """
    return 1 / (1 + math.exp(-parameters[0] * (x - parameters[1])))

def singleton(x, parameters):
    """
    Singleton membership function, a single value fuzzy function

    :param x:
    :param parameters: a list with one real value
    :returns
    """
    return 1 if x == parameters[0] else 0

from bisect import bisect_left, bisect_right

#
# Original Source Code: https://code.activestate.com/recipes/577197-sortedcollection/
# Author: RAYMOND HETTINGER

class SortedCollection(object):
    '''Sequence sorted by a key function.

    SortedCollection() is much easier to work with than using bisect() directly.
    It supports key functions like those use in sorted(), min(), and max().
    The result of the key function call is saved so that keys can be searched
    efficiently.

    Instead of returning an insertion-point which can be hard to interpret, the
    five find-methods return a specific item in the sequence. They can scan for
    exact matches, the last item less-than-or-equal to a key, or the first item
    greater-than-or-equal to a key.

    Once found, an item's ordinal position can be located with the index() method.
    New items can be added with the insert() and insert_right() methods.
    Old items can be deleted with the remove() method.

    The usual sequence methods are provided to support indexing, slicing,
    length lookup, clearing, copying, forward and reverse iteration, contains
    checking, item counts, item removal, and a nice looking repr.

    Finding and indexing are O(log n) operations while iteration and insertion
    are O(n).  The initial sort is O(n log n).

    The key function is stored in the 'key' attibute for easy introspection or
    so that you can assign a new key function (triggering an automatic re-sort).

    In short, the class was designed to handle all of the common use cases for
    bisect but with a simpler API and support for key functions.

    >>> from pprint import pprint
    >>> from operator import itemgetter

    >>> s = SortedCollection(key=itemgetter(2))
    >>> for record in [
    ...         ('roger', 'young', 30),
    ...         ('angela', 'jones', 28),
    ...         ('bill', 'smith', 22),
    ...         ('david', 'thomas', 32)]:
    ...     s.insert(record)

    >>> pprint(list(s))         # show records sorted by age
    [('bill', 'smith', 22),
     ('angela', 'jones', 28),
     ('roger', 'young', 30),
     ('david', 'thomas', 32)]

    >>> s.find_le(29)           # find oldest person aged 29 or younger
    ('angela', 'jones', 28)
    >>> s.find_lt(28)           # find oldest person under 28
    ('bill', 'smith', 22)
    >>> s.find_gt(28)           # find youngest person over 28
    ('roger', 'young', 30)

    >>> r = s.find_ge(32)       # find youngest person aged 32 or older
    >>> s.index(r)              # get the index of their record
    3
    >>> s[3]                    # fetch the record at that index
    ('david', 'thomas', 32)

    >>> s.key = itemgetter(0)   # now sort by first name
    >>> pprint(list(s))
    [('angela', 'jones', 28),
     ('bill', 'smith', 22),
     ('david', 'thomas', 32),
     ('roger', 'young', 30)]

    '''

    def __init__(self, iterable=(), key=None):
        self._given_key = key
        key = (lambda x: x) if key is None else key
        decorated = sorted((key(item), item) for item in iterable)
        self._keys = [k for k, item in decorated]
        self._items = [item for k, item in decorated]
        self._key = key

    def _getkey(self):
        return self._key

    def _setkey(self, key):
        if key is not self._key:
            self.__init__(self._items, key=key)

    def _delkey(self):
        self._setkey(None)

    key = property(_getkey, _setkey, _delkey, 'key function')

    def clear(self):
        self.__init__([], self._key)

    def copy(self):
        return self.__class__(self, self._key)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, i):
        return self._items[i]

    def __iter__(self):
        return iter(self._items)

    def __reversed__(self):
        return reversed(self._items)

    def __repr__(self):
        return '%s(%r, key=%s)' % (
            self.__class__.__name__,
            self._items,
            getattr(self._given_key, '__name__', repr(self._given_key))
        )

    def __reduce__(self):
        return self.__class__, (self._items, self._given_key)

    def __contains__(self, item):
        k = self._key(item)
        i = bisect_left(self._keys, k)
        j = bisect_right(self._keys, k)
        return item in self._items[i:j]

    def index(self, item):
        'Find the position of an item.  Raise ValueError if not found.'
        return self._items[i:j].index(item) + i

    def count(self, item):
        'Return number of occurrences of item'
        return self._items[i:j].count(item)

    def insert(self, item):
        'Insert a new item.  If equal keys are found, add to the left'
        self._keys.insert(i, k)
        self._items.insert(i, item)

    def insert_right(self, item):
        'Insert a new item.  If equal keys are found, add to the right'
        i = bisect_right(self._keys, k)

    def remove(self, item):
        'Remove first occurence of item.  Raise ValueError if not found'
        i = self.index(item)
        del self._keys[i]
        del self._items[i]

    def find(self, k):
        'Return first item with a key == k.  Raise ValueError if not found.'
        if i != len(self) and self._keys[i] == k:
            return self._items[i]
        raise ValueError('No item found with key equal to: %r' % (k,))

    def find_le(self, k):
        'Return last item with a key <= k.  Raise ValueError if not found.'
        if i:
            return self._items[i-1]
        raise ValueError('No item found with key at or below: %r' % (k,))

    def find_lt(self, k):
        'Return last item with a key < k.  Raise ValueError if not found.'
        if i:
        raise ValueError('No item found with key below: %r' % (k,))

    def find_ge(self, k):
        'Return first item with a key >= equal to k.  Raise ValueError if not found'
        if i != len(self):
        raise ValueError('No item found with key at or above: %r' % (k,))

    def find_gt(self, k):
        'Return first item with a key > k.  Raise ValueError if not found'
        raise ValueError('No item found with key above: %r' % (k,))

    def between(self, ge, le):
        g = bisect_left(self._keys, ge)
        l = bisect_right(self._keys, le)
        if g != len(self) and l != len(self):
            return self._items[g : l]
        raise ValueError('No item found between keys : %r,%r' % (ge,le))

    def inside(self, ge, le):
        if g != len(self) and l != len(self) and g != l:
        elif g != len(self) and l != len(self) and g == l:
            return [ self._items[g] ]
        elif g != len(self):
            return self._items[g-1: l]
        elif l != len(self):
            return self._items[g: l-1]
        else:
            return self._items[g - 1: l - 1]
        raise ValueError('No item found inside keys: %r,%r' % (ge,le))

    def around(self, k):
        g = bisect_right(self._keys, k)
        l = bisect_left(self._keys, k)
        raise ValueError('No item found around key : %r' % (k,))

class FLRG(object):
    """
    Fuzzy Logical Relationship Group

    Group a set of FLR's with the same LHS. Represents the temporal patterns for time t+1 (the RHS fuzzy sets)
    when the LHS pattern is identified on time t.
    """

    def __init__(self, order, **kwargs):
        self.LHS = None
        """Left Hand Side of the rule"""
        self.RHS = None
        """Right Hand Side of the rule"""
        self.order = order
        """Number of lags on LHS"""
        self.midpoint = None
        self.key = None

    def append_rhs(self, set, **kwargs):
        pass

    def get_key(self):
        """Returns a unique identifier for this FLRG"""
        if self.key is None:
            if isinstance(self.LHS, (list, set)):
                names = [c for c in self.LHS]
            elif isinstance(self.LHS, dict):
                names = [self.LHS[k] for k in self.LHS.keys()]
            else:
                names = [self.LHS]

            self.key = ""

            for n in names:
                if len(self.key) > 0:
                    self.key += ","
                self.key += n
        return self.key

    def get_membership(self, data, sets):
        """
        Returns the membership value of the FLRG for the input data

        :param data: input data
        :param sets: fuzzy sets
        :return: the membership value
        """
        ret = 0.0
        if isinstance(self.LHS, (list, set)):
            if len(self.LHS) == len(data):
                ret = np.nanmin([sets[self.LHS[ct]].membership(dat) for ct, dat in enumerate(data)])
        else:
            ret = sets[self.LHS].membership(data)
        return ret

    def get_midpoint(self, sets):
        """
        Returns the midpoint value for the RHS fuzzy sets

        :return: the midpoint value
        """
        if self.midpoint is None:
            self.midpoint = np.nanmean(self.get_midpoints(sets))
        return self.midpoint

    def get_midpoints(self, sets):
        if isinstance(self.RHS, (list, set)):
            return np.array([sets[s].centroid for s in self.RHS])
        elif isinstance(self.RHS, dict):
            return np.array([sets[s].centroid for s in self.RHS.keys()])

    def get_lower(self, sets):
        """
        Returns the lower bound value for the RHS fuzzy sets

        :return: lower bound value
        """
        if self.lower is None:
            if isinstance(self.RHS, list):
                self.lower = min([sets[rhs].lower for rhs in self.RHS])
            elif isinstance(self.RHS, dict):
                self.lower = min([sets[self.RHS[s]].lower for s in self.RHS.keys()])
        return self.lower

    def get_upper(self, sets):
        """
        Returns the upper bound value for the RHS fuzzy sets

        :return: upper bound value
        """
        if self.upper is None:
                self.upper = max([sets[rhs].upper for rhs in self.RHS])
                self.upper = max([sets[self.RHS[s]].upper for s in self.RHS.keys()])
        return self.upper

        return len(self.RHS)

    def reset_calculated_values(self):

"""
Tree data structure
"""

from functools import reduce

class FLRGTreeNode:
    """
    Tree node for
    """

    def __init__(self, value):
        self.isRoot = False
        self.children = []
        self.value = value

    def appendChild(self, child):
        self.children.append(child)

    def getChildren(self):
        for child in self.children:
            yield child

    def paths(self, acc=[]):
        if len(self.children) == 0:
            yield [self.value] + acc

            for leaf_path in child.paths([self.value] + acc):  # these two
                yield leaf_path

    def getStr(self, k):
        if self.isRoot:
            tmp = str(self.value)
        else:
            tmp = "\\" + ("-" * k) + str(self.value)
        for child in self.getChildren():
            tmp = tmp + "\n" + child.getStr(k + 1)
        return tmp

        return self.getStr(0)

class FLRGTree:
    """Represents a FLRG set with a tree structure"""
    def __init__(self):
        self.root = FLRGTreeNode(None)

def flat(dados):
    for inst in dados:
        if isinstance(inst, (list, tuple)):
            x = flat(inst)
            for k in x:
                yield k
        else:
            yield inst

def build_tree_without_order(node, lags, level):

    if level not in lags:
        return

    for s in lags[level]:
        node.appendChild(FLRGTreeNode(s))

    for child in node.getChildren():
        build_tree_without_order(child, lags, level + 1)

from pyFTS.common.transformations.transformation import Transformation 

class BoxCox(Transformation):
    """
    Box-Cox power transformation

    y'(t) = log( y(t) )
    y(t) = exp( y'(t) )
    """
    def __init__(self, plambda):
        super(BoxCox, self).__init__()
        self.plambda = plambda
        self.name = 'BoxCox'

    @property
    def parameters(self):
        return self.plambda

    def apply(self, data, param=None, **kwargs):
        if self.plambda != 0:
            modified = [(dat ** self.plambda - 1) / self.plambda for dat in data]
        else:
            modified = [np.log(dat) for dat in data]
        return np.array(modified)

    def inverse(self, data, param=None, **kwargs):
            modified = [np.exp(np.log(dat * self.plambda + 1) ) / self.plambda for dat in data]
        else:
            modified = [np.exp(dat) for dat in data]

class Differential(Transformation):
    """
    Differentiation data transform

    y'(t) = y(t) - y(t-1)
    y(t) =  y(t-1)  + y'(t)
    """
    def __init__(self, lag):
        super(Differential, self).__init__()
        self.lag = lag
        self.minimal_length = 2
        self.name = 'Diff'

    @property
        return self.lag

        if param is not None:
            self.lag = param

        if not isinstance(data, (list, np.ndarray, np.generic)):
            data = [data]

        if isinstance(data, (np.ndarray, np.generic)):
            data = data.tolist()

        n = len(data)
        diff = [data[t] - data[t - self.lag] for t in np.arange(self.lag, n)]
        for t in np.arange(0, self.lag): diff.insert(0, 0)
        return diff

    def inverse(self, data, param, **kwargs):

        type = kwargs.get("type","point")
        steps_ahead = kwargs.get("steps_ahead", 1)

        if not isinstance(data, list):

#        print(n)
#        print(len(param))

        if steps_ahead == 1:
            if type == "point":
                inc = [data[t] + param[t] for t in np.arange(0, n)]
            elif type == "interval":
                inc = [[data[t][0] + param[t], data[t][1] + param[t]] for t in np.arange(0, n)]
            elif type == "distribution":
                for t in np.arange(0, n):
                    data[t].differential_offset(param[t])
                inc = data
        else:
                inc = [data[0] + param[0]]
                for t in np.arange(1, steps_ahead):
                    inc.append(data[t] + inc[t-1])
                inc = [[data[0][0] + param[0], data[0][1] + param[0]]]
                    inc.append([data[t][0] + np.nanmean(inc[t-1]), data[t][1] + np.nanmean(inc[t-1])])
                data[0].differential_offset(param[0])
                    ex = data[t-1].expected_value()
                    data[t].differential_offset(ex)
                inc = data

        if n == 1:
            return inc[0]
        else:
            return inc

class Normalization(Transformation):
        super(Normalization, self).__init__()
        self.name = 'Normalization'

        self.mu = 0
        self.sigma = 0
        
    def train(self, data, **kwargs):
        self.mu = np.mean(data)
        self.sigma = np.std(data)

        modified = (data - self.mu) / self.sigma
        return modified

        modified = (data * self.sigma) + self.mu

class ROI(Transformation):
    """
    Return of Investment (ROI) transformation. Retrieved from Sadaei and Lee (2014) - Multilayer Stock
    Forecasting Model Using Fuzzy Time Series

    y'(t) = ( y(t) - y(t-1) ) / y(t-1)
    y(t) = ( y(t-1) * y'(t) ) + y(t-1)
    """
        super(ROI, self).__init__()
        self.name = 'ROI'

        modified = [(data[i] - data[i - 1]) / data[i - 1] for i in np.arange(1, len(data))]
        modified.insert(0, .0)

        modified = [(param[i - 1] * data[i]) + param[i - 1] for i in np.arange(1, len(data))]

class Scale(Transformation):
    """
    Scale data inside a interval [min, max]

    """
    def __init__(self, min=0, max=1):
        super(Scale, self).__init__()
        self.data_max = None
        self.data_min = None
        self.transf_max = max
        self.transf_min = min
        self.name = 'Scale'

    @property
        return [self.transf_max, self.transf_min]

    def apply(self, data, param=None,**kwargs):
        if self.data_max is None:
            self.data_max = np.nanmax(data)
            self.data_min = np.nanmin(data)
        data_range = self.data_max - self.data_min
        transf_range = self.transf_max - self.transf_min
        if isinstance(data, list):
            tmp = [(k + (-1 * self.data_min)) / data_range for k in data]
            tmp2 = [ (k * transf_range) + self.transf_min for k in tmp]
        else:
            tmp = (data + (-1 * self.data_min)) / data_range
            tmp2 = (tmp * transf_range) + self.transf_min

        return  tmp2

            tmp2 = [(k - self.transf_min) / transf_range   for k in data]
            tmp = [(k * data_range) + self.data_min for k in tmp2]
        else:
            tmp2 = (data - self.transf_min) / transf_range
            tmp = (tmp2 * data_range) + self.data_min
        return tmp

class MovingAverage(Transformation):
        super(MovingAverage, self).__init__(**kwargs)
        self.name = 'Moving Average Smoothing'
        self.steps = kwargs.get('steps',2)

        steps = param if param is not None else self.steps
        ma = [k for k in data[:steps]]
        ma.extend([np.mean(data[k-steps:k]) for k in range(steps, len(data))])
        return ma

class ExponentialSmoothing(Transformation):
        super(ExponentialSmoothing,self).__init__(**kwargs)
        self.name = 'Exponential Moving Average Smoothing'
        self.beta = kwargs.get('beta',.5)

        beta = kwargs.get('beta',None)
        beta = beta if beta is not None else self.beta
        mm = [k for k in data[:steps]]
        for i in range(steps, len(data)):
            ret = 0
            for k in np.arange(0,steps):
                ret += ( beta * (1 - beta) ** k ) * data[i - k]
            mm.append(ret)
        return mm

class AveragePooling(Transformation):
        super(AveragePooling,self).__init__(**kwargs)
        self.name = 'Exponential Average Smoothing'
        self.kernel = kwargs.get('kernel',5)
        self.stride = kwargs.get('stride',1)
        self.padding = kwargs.get('padding','same')

    def apply(self, data):
        result = []
        if self.padding == 'same':
            for i in range(int(self.kernel/2), len(data)+int(self.kernel/2), self.stride):
                result.append(np.mean(data[np.max([0,i-self.kernel]):np.min([i, len(data)])]))

        elif self.padding == 'valid':
            for i in range(self.kernel, len(data), self.stride):
                result.append(np.mean(data[i-self.kernel:i]))
        else:
            raise ValueError('Invalid padding schema')
        return result

class MaxPooling(Transformation):
        super(MaxPooling,self).__init__(**kwargs)

        result = []
                result.append(np.max(data[np.max([0,i-self.kernel]):np.min([i, len(data)])]))

            for i in range(self.kernel - 1, len(data), self.stride):
                result.append(np.max(data[i-self.kernel:i]))
        else:

"""
Kohonen Self Organizing Maps for Fuzzy Time Series
"""
#from pyFTS.models.multivariate import wmvfts
from typing import Tuple
from typing import List
from pyFTS.common.transformations.transformation import Transformation

class SOMTransformation(Transformation):
    
    def __init__(self,
                 grid_dimension: Tuple,
                 **kwargs):

        import SimpSOM as sps

        # SOM attributes
        self.load_file = kwargs.get('loadFile')
        self.net: sps.somNet = None
        self.data: pd.DataFrame = None
        self.grid_dimension: Tuple = grid_dimension
        self.pbc = kwargs.get('PBC', True)
        self.is_multivariate = True

        # debug attributes
        self.name = 'Kohonen Self Organizing Maps FTS'
        self.shortname = 'SOM-FTS'

    def apply(self,
              data: pd.DataFrame,
              param=None,
              **kwargs):
        """
        Transform a M-dimensional dataset into a 3-dimensional dataset, where one dimension is the endogen variable
        If endogen_variable = None, the last column will be the endogen_variable.

        Args:
            data (pd.DataFrame): M-Dimensional dataset
            endogen_variable (str):  column of dataset
            names (Tuple): names for new columns created by SOM Transformation.
            param:
            **kwargs: params of SOM's train process
                percentage_train (float). Percentage of dataset that will be used for train SOM network. default: 0.7
                leaning_rate (float): leaning rate of SOM network. default: 0.01
                epochs: epochs of SOM network. default: 10000
        
        Returns:

        """

        endogen_variable = kwargs.get('endogen_variable', None)
        names = kwargs.get('names', ('x', 'y'))

        if endogen_variable not in data.columns:
            endogen_variable = None
        cols = data.columns[:-1] if endogen_variable is None else [col for col in data.columns if
                                                                   col != endogen_variable]
        if self.net is None:
            train = data[cols]
            self.train(data=train, **kwargs)
        new_data = self.net.project(data[cols].values)
        new_data = pd.DataFrame(new_data, columns=names)
        endogen = endogen_variable if endogen_variable is not None else data.columns[-1]
        new_data[endogen] = data[endogen].values
        return new_data

        status = "is trained" if self.net is not None else "not trained"
        return f'{self.name}-{status}'

        return self.name

    def __del__(self):
        del self.net

    def train(self,
              percentage_train: float = .7,
              leaning_rate: float = 0.01,
              epochs: int = 10000):
        data = data.dropna()
        self.data = data.values
        limit = round(len(self.data) * percentage_train)
        train = self.data[:limit]
        x, y = self.grid_dimension
        self.net = sps.somNet(x, y, train, PBC=self.pbc,
                              loadFile=self.load_file)
        self.net.train(startLearnRate=leaning_rate,
                       epochs=epochs)

    def save_net(self,
                 filename: str = "SomNet trained"):
        self.net.save(filename)
        self.load_file = filename

    def show_grid(self,
                  graph_type: str = 'nodes_graph',
                  **kwargs):
        if graph_type == 'nodes_graph':
            colnum = kwargs.get('colnum', 0)
            self.net.nodes_graph(colnum=colnum)
        else:
            self.net.diff_graph()

class Transformation(object):
    """
    Data transformation used on pre and post processing of the FTS
    """

        self.is_invertible = True
        self.is_multivariate = False
        """detemine if this transformation can be applied to multivariate data"""
        self.minimal_length = 1
        self.name = ''

    def apply(self, data, param, **kwargs):
        """
        Apply the transformation on input data

        :param param:
        :param kwargs:
        :return: numpy array with transformed data
        """
        pass

    def inverse(self,data, param, **kwargs):
        """

        :param data: transformed data
        :return: numpy array with inverse transformed data
        """
        pass

"""
Common facilities for hyperparameter optimization
"""

import sqlite3

def open_hyperparam_db(name):
    """
    Open a connection with a Sqlite database designed to store benchmark results.

    :param name: database filenem
    :return: a sqlite3 database connection
    """
    conn = sqlite3.connect(name)

    #performance optimizations
    conn.execute("PRAGMA journal_mode = WAL")
    conn.execute("PRAGMA synchronous = NORMAL")

    create_hyperparam_tables(conn)
    return conn

def create_hyperparam_tables(conn):
    """
    Create a sqlite3 table designed to store benchmark results.

    :param conn: a sqlite3 database connection
    """
    c = conn.cursor()

    c.execute('''CREATE TABLE if not exists hyperparam(
                 ID integer primary key, Date int, Dataset text, Tag text, 
                 Model text, Transformation text, mf text, 'Order' int, 
                 Partitioner text, Partitions int, alpha real, lags text, 
                 Measure text, Value real)''')

    conn.commit()

def insert_hyperparam(data, conn):
    """
    Insert benchmark data on database

    :param data: a tuple with the benchmark data with format:

    Dataset: Identify on which dataset the dataset was performed
    Tag: a user defined word that indentify a benchmark set
    Model: FTS model
    Transformation: The name of data transformation, if one was used
    mf: membership function
    Order: the order of the FTS method
    Partitioner: UoD partitioning scheme
    Partitions: Number of partitions
    alpha: alpha cut
    lags: lags
    Measure: accuracy measure
    Value: the measure value

    :return:
    """

    c.execute("INSERT INTO hyperparam(Date, Dataset, Tag, Model, "
              + "Transformation, mf, 'Order', Partitioner, Partitions, "
              + "alpha, lags, Measure, Value) "
              + "VALUES(datetime('now'),?,?,?,?,?,?,?,?,?,?,?,?)", data)

    """Multivariate Fuzzy Logical Relationship"""

        """
        :param LHS: Left Hand Side fuzzy set
        :param RHS: Right Hand Side fuzzy set
        """
        self.LHS = {}

    def set_lhs(self, var, set):
        self.LHS[var] = set

    def set_rhs(self, set):
        self.RHS = set

        return "{} -> {}".format([k for k in self.LHS.values()], self.RHS)

from pyFTS.common import FuzzySet, Composite

class MultivariateFuzzySet(Composite.FuzzySet):
    """
    Multivariate Composite Fuzzy Set
    """
        """
        """
        super(MultivariateFuzzySet, self).__init__("")
        self.sets = {}
        self.target_variable = kwargs.get('target_variable',None)

    def append_set(self, variable, set):
        """
        Appends a new fuzzy set from a new variable

        :param variable: an multivariate.variable instance
        :param set: an common.FuzzySet instance
        """
        self.sets[variable] = set

        if variable == self.target_variable.name:

        self.name += set.name

    def set_target_variable(self, variable):
        self.target_variable = variable
        self.centroid = self.sets[variable.name].centroid
        self.upper = self.sets[variable.name].upper
        self.lower = self.sets[variable.name].lower

        mv = []
        if isinstance(x, (dict, pd.DataFrame)):
            for var in self.sets.keys():
                data = x[var]
                mv.append(self.sets[var].membership(data))
        else:
            mv = [self.sets[self.target_variable.name].membership(x)]

        return np.nanmin(mv)

def fuzzyfy_instance(data_point, var, tuples=True):
    #try:
    fsets = var.partitioner.fuzzyfy(data_point, mode='sets', method='fuzzy', alpha_cut=var.alpha_cut)
    if tuples:
        return [(var.name, fs) for fs in fsets]
    else:
        return fsets
    #except Exception as ex:
    # print(data_point)

def fuzzyfy_instance_clustered(data_point, cluster, **kwargs):
    alpha_cut = kwargs.get('alpha_cut', 0.0)
    fsets = []
    for fset in cluster.search(data_point, type='name'):
        if cluster.sets[fset].membership(data_point) >= alpha_cut:
            if mode == 'sets':
                fsets.append(fset)
            elif mode =='both':
                fsets.append( (fset, cluster.sets[fset].membership(data_point)) )
    return fsets

from pyFTS.common import flrg as flg

class FLRG(flg.FLRG):
    """
    Multivariate Fuzzy Logical Rule Group
    """

        super(FLRG,self).__init__(0,**kwargs)
        self.order = kwargs.get('order', 1)
        self.LHS = kwargs.get('lhs', {})
        self.RHS = set()

    def set_lhs(self, var, fset):
        if self.order == 1:
            self.LHS[var] = fset
        else:
            if var not in self.LHS:
                self.LHS[var] = []
            self.LHS[var].append(fset)

    def append_rhs(self, fset, **kwargs):
        self.RHS.add(fset)

    def get_membership(self, data, variables):
        mvs = []
        for var in variables:
            s = self.LHS[var.name]
            mvs.append(var.partitioner.sets[s].membership(data[var.name]))

        return np.nanmin(mvs)

            self.lower = min([sets[rhs].lower for rhs in self.RHS])

            self.upper = max([sets[rhs].upper for rhs in self.RHS])

        _str = ""
        for k in self.RHS:
            _str += "," if len(_str) > 0 else ""
            _str += k

        return self.get_key() + " -> " + _str

from pyFTS.partitioners import partitioner
from pyFTS.models.multivariate.common import MultivariateFuzzySet, fuzzyfy_instance_clustered
from itertools import product
from scipy.spatial import KDTree

class MultivariatePartitioner(partitioner.Partitioner):
    """
    Base class for partitioners which use the MultivariateFuzzySet
    """

        super(MultivariatePartitioner, self).__init__(name="MultivariatePartitioner", preprocess=False, **kwargs)

        self.type = 'multivariate'
        self.kdtree = None
        self.index = {}
        self.explanatory_variables = kwargs.get('explanatory_variables', [])
        self.target_variable = kwargs.get('target_variable', None)
        self.neighbors = kwargs.get('neighbors', 2)
        self.optimize = kwargs.get('optimize', True)
        if self.optimize:
            self.count = {}
        data = kwargs.get('data', None)
        self.build(data)
        self.uod = {}

        self.min = self.target_variable.partitioner.min
        self.max = self.target_variable.partitioner.max

    def format_data(self, data):
        ndata = {}
        for var in self.explanatory_variables:
            ndata[var.name] = var.partitioner.extractor(data[var.data_label])

        return ndata

    def build(self, data):
        pass

    def append(self, fset):
        self.sets[fset.name] = fset

    def prune(self):

        if not self.optimize:
            return

        for fset in [fs for fs in self.sets.keys()]:
            if fset not in self.count:
                fs = self.sets.pop(fset)
                del (fs)

        self.build_index()

    def search(self, data, **kwargs):
        """
        Perform a search for the nearest fuzzy sets of the point 'data'. This function were designed to work with several
        overlapped fuzzy sets.

        :param data: the value to search for the nearest fuzzy sets
        :param type: the return type: 'index' for the fuzzy set indexes or 'name' for fuzzy set names.
        :return: a list with the nearest fuzzy sets
        """

        if self.kdtree is None:
            self.build_index()

        type = kwargs.get('type', 'index')

        ndata = [data[k.name] for k in self.explanatory_variables]
        _, ix = self.kdtree.query(ndata, self.neighbors)

        if not isinstance(ix, (list, np.ndarray)):
            ix = [ix]

            tmp = []
            for k in ix:
                tmp.append(self.index[k])
                self.count[self.index[k]] = 1

        if type == 'name':
            return [self.index[k] for k in ix]
        elif type == 'index':
            return sorted(ix)

    def fuzzyfy(self, data, **kwargs):
        return fuzzyfy_instance_clustered(data, self, **kwargs)

    def change_target_variable(self, variable):
        for fset in self.sets.values():
            fset.set_target_variable(variable)
        self.min = variable.partitioner.min
        self.max = variable.partitioner.max

    def build_index(self):

        midpoints = []

        for ct, fset in enumerate(self.sets.values()):
            mp = []
            for vr in self.explanatory_variables:
                mp.append(fset.sets[vr.name].centroid)
            midpoints.append(mp)
            self.index[ct] = fset.name

        import sys
        sys.setrecursionlimit(100000)

        self.kdtree = KDTree(midpoints)

        sys.setrecursionlimit(1000)

from pyFTS.models.multivariate import partitioner

class GridCluster(partitioner.MultivariatePartitioner):
    """
    A cartesian product of all fuzzy sets of all variables
    """

        super(GridCluster, self).__init__(**kwargs)
        self.name="GridCluster"
        self.build(None)

        fsets = [[x for x in k.partitioner.sets.values()]
                 for k in self.explanatory_variables]
        c = 0
        for k in product(*fsets):
            mvfset = MultivariateFuzzySet(target_variable=self.target_variable)
            for fset in k:
                mvfset.append_set(fset.variable, fset)

            self.sets[mvfset.name] = mvfset
            c += 1

    def defuzzyfy(self, values, mode='both'):
        if not isinstance(values, list):
            values = [values]

        ret = []
        for val in values:
            if mode == 'both':
                num = []
                den = []
                for fset, mv in val:
                    num.append(self.sets[fset].centroid * mv)
                    den.append(mv)
                ret.append(np.nansum(num) / np.nansum(den))
            elif mode == 'both':
                num = np.mean([self.sets[fset].centroid for fset in val])
                ret.append(num)
            elif mode == 'vector':
                num = []
                den = []
                for fset, mv in enumerate(val):
                    num.append(self.sets[self.ordered_sets[fset]].centroid * mv)
            else:
                raise Exception('Unknown deffuzyfication mode')

        return ret

class IncrementalGridCluster(partitioner.MultivariatePartitioner):
    """
    Create combinations of fuzzy sets of the variables on demand, incrementally increasing the
    multivariate fuzzy set base.
    """

        super(IncrementalGridCluster, self).__init__(**kwargs)
        self.name="IncrementalGridCluster"

        if isinstance(data, pd.DataFrame):
            ret = []
            for index, inst in data.iterrows():
                mv = self.fuzzyfy(inst, **kwargs)
                ret.append(mv)
            return ret

        if self.kdtree is not None:
            fsets = self.search(data, type='name')
        else:
            fsets = self.incremental_search(data, type='name')

        mode = kwargs.get('mode', 'sets')
        if mode == 'sets':
        elif mode == 'vector':
            raise NotImplementedError()
            ret = []
            for key in fsets:
                mvfset = self.sets[key]
                ret.append((key, mvfset.membership(data)))

        return ret

    def incremental_search(self, data, **kwargs):
        alpha_cut = kwargs.get('alpha_cut', 0.)

        fsets = {}
        ret = []
            ac = alpha_cut if alpha_cut > 0. else var.alpha_cut
            fsets[var.name] = var.partitioner.fuzzyfy(data[var.name], mode=mode, alpha_cut=ac)

        fsets_by_var = [fsets for var, fsets in fsets.items()]

        for p in product(*fsets_by_var):
                path = [fset for fset, mv in p]
                mv = [mv for fset, mv in p]
                key = ''.join(path)
            elif mode == 'sets':
                key = ''.join(p)
                path = p
            if key not in self.sets:
                mvfset = MultivariateFuzzySet(target_variable=self.target_variable)
                for ct, fs in enumerate(path):
                    mvfset.append_set(self.explanatory_variables[ct].name,
                                      self.explanatory_variables[ct].partitioner[fs])
                mvfset.name = key
                self.sets[key] = mvfset

                ret.append(key)
                ret.append( tuple(key,np.nanmin(mv)) )

        return ret

"""
Non Stationary Fuzzy Sets

GARIBALDI, Jonathan M.; JAROSZEWSKI, Marcin; MUSIKASUWAN, Salang. Nonstationary fuzzy sets.
IEEE Transactions on Fuzzy Systems, v. 16, n. 4, p. 1072-1086, 2008.
"""

from pyFTS.common import FuzzySet as FS, Membership, FLR
from pyFTS.models.nonstationary import perturbation

class FuzzySet(FS.FuzzySet):
    """
    Non Stationary Fuzzy Sets
    """

    def __init__(self, name, mf, parameters, **kwargs):
        """
        Constructor
        """
        super(FuzzySet, self).__init__(name=name, mf=mf, parameters=parameters, centroid=None, alpha=1.0, **kwargs)
    
        self.location = kwargs.get("location", None)
        """Pertubation function that affects the location of the membership function"""
        self.location_params = kwargs.get("location_params", None)
        """Parameters for location pertubation function"""
        self.location_roots = kwargs.get("location_roots", 0)
        self.width = kwargs.get("width", None)
        """Pertubation function that affects the width of the membership function"""
        self.width_params = kwargs.get("width_params", None)
        """Parameters for width pertubation function"""
        self.width_roots = kwargs.get("width_roots", 0)
        self.noise = kwargs.get("noise", None)
        """Pertubation function that adds noise on the membership function"""
        self.noise_params = kwargs.get("noise_params", None)
        """Parameters for noise pertubation function"""
        self.perturbated_parameters = {}
        self.type = 'nonstationary'
    
        if self.location is not None and not isinstance(self.location, (list, set)):
            self.location = [self.location]
            self.location_params = [self.location_params]
            self.location_roots = [self.location_roots]
    
        if self.width is not None and not isinstance(self.width, (list, set)):
            self.width = [self.width]
            self.width_params = [self.width_params]
            self.width_roots = [self.width_roots]
    
    def perform_location(self, t, param):
        if self.location is None:
            inc = t
        else:
            l = len(self.location)
            inc = sum([self.location[k](t + self.location_roots[k], self.location_params[k]) for k in np.arange(0, l)])

        if self.mf == Membership.gaussmf:
            # changes only the mean parameter
            return [param[0] + inc, param[1]]
        elif self.mf == Membership.sigmf:
            # changes only the midpoint parameter
            return [param[0], param[1] + inc]
        elif self.mf == Membership.bellmf:
            return [param[0], param[1], param[2] + inc]
        else:
            # translate all parameters
            return [k + inc for k in param]
    
    def perform_width(self, t, param):
        if self.width is None:
            inc = t
        else:

            l = len(self.width)

            inc = sum([self.width[k](t + self.width_roots[k], self.width_params[k]) for k in np.arange(0, l)])

            # changes only the variance parameter
            # changes only the smooth parameter
        elif self.mf == Membership.trimf:
            tmp = inc / 2
            return [param[0] - tmp, param[1], param[2] + tmp]
        elif self.mf == Membership.trapmf:
            l = (param[3] - param[0])
            rab = (param[1] - param[0]) / l
            rcd = (param[3] - param[2]) / l
            return [param[0] - inc, param[1] - inc * rab, param[2] + inc * rcd, param[3] + inc]
        else:
            return param
    
    def membership(self, x, t):
        """

        :param t: time displacement or perturbation parameters
        """
    
        self.perturbate_parameters(t)
    
        tmp = self.mf(x, self.perturbated_parameters[str(t)])
    
        if self.noise is not None:
            tmp += self.noise(t, self.noise_params)
    
        return tmp
    
    def perturbate_parameters(self, t):
        if str(t) not in self.perturbated_parameters:
            param = self.parameters
            if isinstance(t, (list, set)):
                param = self.perform_location(t[0], param)
                param = self.perform_width(t[1], param)
            else:
                param = self.perform_location(t, param)
                param = self.perform_width(t, param)
            self.perturbated_parameters[str(t)] = param
            
    def get_midpoint(self, t):

        param = self.perturbated_parameters[str(t)]

            return param[0]
            return param[1]
            return (param[2] - param[1]) / 2
        else:

    def get_lower(self, t):

            return param[0] - 3*param[1]
            return param[0] - param[1]
        else:

    def get_upper(self, t):

            return param[0] + 3*param[1]
            return param[0] + param[1]
            return param[2]
            return param[3]
        else:

        tmp = ""
        if self.location is not None:
            tmp += "Location: "
            for ct, f in enumerate(self.location):
                tmp += str(f.__name__) + "(" + str(["{0:.2f}".format(p) for p in self.location_params[ct]]) + ") "
        if self.width is not None:
            tmp += "Width: "
            for ct, f in enumerate(self.width):
                tmp += str(f.__name__) + "(" + str(["{0:.2f}".format(p) for p in self.width_params[ct]]) + ") "
        tmp = "(" + str(["{0:.2f}".format(p) for p in self.parameters]) + ") " + tmp
        return self.name + ": " + str(self.mf.__name__) + tmp

def fuzzify(inst, t, fuzzySets):
    """
    Calculate the membership values for a data point given nonstationary fuzzy sets

    :param inst: data points
    :param t: time displacement of the instance
    :param fuzzySets: list of fuzzy sets
    """
    ret = []
    if not isinstance(inst, list):
        inst = [inst]
    for t, i in enumerate(inst):
        mv = np.array([fs.membership(i, t) for fs in fuzzySets])
    return ret

def fuzzySeries(data, fuzzySets, ordered_sets, window_size=1, method='fuzzy', const_t= None):
    fts = []
        tdisp = window_index(t, window_size) if const_t is None else const_t
        mv = np.array([fuzzySets[fs].membership(i, tdisp) for fs in ordered_sets])
            sets = [check_bounds(i, fuzzySets, tdisp)]
        else:
                ix = np.ravel(np.argwhere(mv > 0.0))
            sets = [fuzzySets[ordered_sets[i]] for i in ix]
    return fts

def window_index(t, window_size):
    if isinstance(t, (list, set)):
        return t
    return t - (t % window_size)

def check_bounds(data, partitioner, t):
    if data < partitioner.lower_set().get_lower(t):
        return partitioner.lower_set()
    elif data > partitioner.upper_set().get_upper(t):
        return partitioner.upper_set()

def check_bounds_index(data, partitioner, t):
        return 0
        return len(partitioner.sets) -1

from pyFTS.common import flrg
from pyFTS.models.nonstationary import common

class NonStationaryFLRG(flrg.FLRG):
    def unpack_args(self, *args):
        l = len(args)
        tmp = args
        sets, t, w = None, 0, 1
        if l > 0 and isinstance(tmp[0], dict):
            sets = tmp[0]
        if l > 1 and isinstance(tmp[1], (int, list, tuple)):
            t = tmp[1]
        if l > 2 and isinstance(tmp[2], int):
            w = tmp[2]

        return (sets, t, w)

    def __init__(self, LHS, **kwargs):
        super(NonStationaryFLRG, self).__init__(1, **kwargs)

        if isinstance(self.LHS, list):
            return str([k for k in self.LHS])
        elif isinstance(self.LHS, dict):
            return str(self.LHS.keys())
        else:
            return self.LHS

    def get_membership(self, data, *args):
        sets, t, window_size = self.unpack_args(*args)
        ret = 0.0
            ret = min([sets[self.LHS[ct]].membership(dat, common.window_index(t - (self.order - ct), window_size))
                       for ct, dat in enumerate(data)])
        else:
            ret = self.LHS.membership(data, common.window_index(t, window_size))
        return ret

    def get_midpoint(self, *args):
        if len(self.RHS) > 0:
            if isinstance(self.RHS, (list, set)):
                tmp = [sets[r].get_midpoint(common.window_index(t, window_size)) for r in self.RHS]
                tmp = [sets[r].get_midpoint(common.window_index(t, window_size)) for r in self.RHS.keys()]
            return sum(tmp) / len(tmp)
        else:
            return sets[self.LHS[-1]].get_midpoint(common.window_index(t, window_size))

    def get_lower(self, *args):
                return min([sets[r].get_lower(common.window_index(t, window_size)) for r in self.RHS])
                return min([sets[r].get_lower(common.window_index(t, window_size)) for r in self.RHS.keys()])
        else:
            return sets[self.LHS[-1]].get_lower(common.window_index(t, window_size))

    def get_upper(self, *args):
                return max([sets[r].get_upper(common.window_index(t, window_size)) for r in self.RHS])
                return max([sets[r].get_upper(common.window_index(t, window_size)) for r in self.RHS.keys()])
        else:
            return sets[self.LHS[-1]].get_upper(common.window_index(t, window_size))

from pyFTS.models.nonstationary import common, perturbation
from pyFTS.common import FuzzySet as stationary_fs

class PolynomialNonStationaryPartitioner(partitioner.Partitioner):
    """
    Non Stationary Universe of Discourse Partitioner
    """

    def __init__(self, data, part, **kwargs):
        """"""
        super(PolynomialNonStationaryPartitioner, self).__init__(name=part.name, data=data, npart=part.partitions,
                                                                 func=part.membership_function, names=part.setnames,
                                                                 prefix=part.prefix, transformation=part.transformation,
                                                                 indexer=part.indexer, preprocess=False)

        loc_params, wid_params = self.get_polynomial_perturbations(data, **kwargs)

        if self.ordered_sets is None and self.setnames is not None:
            self.ordered_sets = part.setnames
        else:
            self.ordered_sets = stationary_fs.set_ordered(part.sets)

        for ct, key in enumerate(self.ordered_sets):
            set = part.sets[key]
            loc_roots = np.roots(loc_params[ct])[0]
            wid_roots = np.roots(wid_params[ct])[0]
            tmp = common.FuzzySet(set.name, set.mf, set.parameters,
                           location=perturbation.polynomial,
                           location_params=loc_params[ct],
                           location_roots=loc_roots, #**kwargs)
                           width=perturbation.polynomial,
                           width_params=wid_params[ct],
                           width_roots=wid_roots, **kwargs)

            self.sets[set.name] = tmp

    def poly_width(self, par1, par2, rng, deg):
        a = np.polyval(par1, rng)
        b = np.polyval(par2, rng)
        diff = [b[k] - a[k] for k in rng]
        tmp = np.polyfit(rng, diff, deg=deg)
        return tmp

    def scale_up(self,x,pct):
        if x > 0: return x*(1+pct)
        else: return x*pct

    def scale_down(self,x,pct):
        if x > 0: return x*pct
        else: return x*(1+pct)

    def get_polynomial_perturbations(self, data, **kwargs):
        w = kwargs.get("window_size", int(len(data) / 5))
        degree = kwargs.get("degree", 2)
        xmax = [data[0]]
        tmax = [0]
        xmin = [data[0]]
        tmin = [0]

        l = len(data)

        for i in np.arange(0, l, w):
            sample = data[i:i + w]
            tx = max(sample)
            xmax.append(tx)
            tmax.append(np.ravel(np.argwhere(data == tx)).tolist()[0])
            tn = min(sample)
            xmin.append(tn)
            tmin.append(np.ravel(np.argwhere(data == tn)).tolist()[0])

        cmax = np.polyfit(tmax, xmax, deg=degree)
        cmin = np.polyfit(tmin, xmin, deg=degree)

        cmed = []

        for d in np.arange(0, degree + 1):
            cmed.append(np.linspace(cmin[d], cmax[d], self.partitions)[1:self.partitions - 1])

        loc_params = [cmin.tolist()]
        for i in np.arange(0, self.partitions - 2):
            tmp = [cmed[k][i] for k in np.arange(0, degree + 1)]
            loc_params.append(tmp)
        loc_params.append(cmax.tolist())

        rng = np.arange(0, l)

        clen = []

        for i in np.arange(1, self.partitions-1):
            tmp = self.poly_width(loc_params[i - 1], loc_params[i + 1], rng, degree)
            clen.append(tmp)

        tmp = self.poly_width(loc_params[0], loc_params[1], rng, degree)
        clen.insert(0, tmp)

        tmp = self.poly_width(loc_params[self.partitions-2], loc_params[self.partitions-1], rng, degree)
        clen.append(tmp)

        tmp = (loc_params, clen)

        return tmp

        pass

class SimpleNonStationaryPartitioner(partitioner.Partitioner):
    """
    """

        """"""
        super(SimpleNonStationaryPartitioner, self).__init__(name=part.name, data=data, npart=part.partitions,
                                                             func=part.membership_function, names=part.setnames,
                                                             prefix=part.prefix, transformation=part.transformation,
                                                             indexer=part.indexer)#, preprocess=False)
        self.partitions = part.partitions

        for key in part.sets.keys():
            tmp = common.FuzzySet(set.name, set.mf, set.parameters, **kwargs)
            tmp.centroid = set.centroid

            self.sets[key] =tmp

        self.ordered_sets = stationary_fs.set_ordered(self.sets)

        return {}

def simplenonstationary_gridpartitioner_builder(data, npart, transformation):
    from pyFTS.partitioners import Grid
    from pyFTS.models.nonstationary import perturbation, partitioners

    tmp_fs = Grid.GridPartitioner(data=data, npart=npart, transformation=transformation)
    fs = partitioners.SimpleNonStationaryPartitioner(data, tmp_fs,
                                                     location=perturbation.polynomial,
                                                     location_params=[1, 0],
                                                     location_roots=0,
                                                     width=perturbation.polynomial,
                                                     width_params=[1, 0],
                                                     width_roots=0)
    return fs

"""
Pertubation functions for Non Stationary Fuzzy Sets
"""

from pyFTS.common import FuzzySet, Membership

def linear(x, parameters):
    return np.polyval(parameters, x)

def polynomial(x, parameters):
        return np.polyval(parameters, x)

def exponential(x, parameters):
    return np.exp(x*parameters[0])

def periodic(x, parameters):
    return parameters[0] * np.sin(x * parameters[1])

from pyFTS.models.seasonal import common

class SeasonalIndexer(object):
    """
    Seasonal Indexer. Responsible to find the seasonal index of a data point inside its data set
    """
    def __init__(self,num_seasons, **kwargs):
        self.num_seasons = num_seasons
        self.name = kwargs.get("name","")

    def get_season_of_data(self,data):
        pass

    def get_season_by_index(self,inde):
        pass

    def get_data_by_season(self, data, indexes):
        pass

    def get_index_by_season(self, indexes):
        pass

    def get_data(self, data):
        pass

    def get_index(self, data):
        pass

class LinearSeasonalIndexer(SeasonalIndexer):
    """Use the data array/list position to index the seasonality """
    def __init__(self, seasons, units, ignore=None, **kwargs):
        """
        Indexer for array/list position
        :param seasons: A list with the season group (i.e: 7 for week, 30 for month, etc)
        :param units: A list with the units used for each season group, the default is 1 for each
        :param ignore:
        """
        super(LinearSeasonalIndexer, self).__init__(len(seasons), **kwargs)
        self.seasons = seasons
        self.units = units
        self.ignore = ignore

        return self.get_season_by_index(np.arange(0, len(data)).tolist())

    def get_season_by_index(self, index):
        ret = []
        if not isinstance(index, (list, np.ndarray)):
            if self.num_seasons == 1:
                season = (index // self.units[0]) % self.seasons[0]
            else:
                season = []
                for ct, seasonality in enumerate(self.seasons, start=0):
                    tmp = (index // self.units[ct]) % self.seasons[ct]
                    if not self.ignore[ct]:
                        season.append(tmp)
            ret.append(season)
        else:
            for ix in index:
                if self.num_seasons == 1:
                    season = (ix  // self.units[0]) % self.seasons[0]
                else:
                    season = []
                    for ct, seasonality in enumerate(self.seasons, start=0):
                        tmp = (ix  // self.units[ct]) % self.seasons[ct]
                        if not self.ignore[ct]:
                            season.append(tmp)
                ret.append(season)

        return ret

        ix = 0;

        for count,season in enumerate(self.seasons):
            ix += season*(indexes[count])

        #ix += indexes[-1]

        return ix

class DataFrameSeasonalIndexer(SeasonalIndexer):
    """Use the Pandas.DataFrame index position to index the seasonality """
    def __init__(self,index_fields,index_seasons, data_field,**kwargs):
        """

        :param index_fields: DataFrame field to use as index
        :param index_seasons: A list with the season group, i. e., multiples of positions that are considered a season (i.e: 7 for week, 30 for month, etc)
        :param data_fields: DataFrame field to use as data
        """
        super(DataFrameSeasonalIndexer, self).__init__(len(index_seasons), **kwargs)
        self.fields = index_fields
        self.seasons = index_seasons
        self.data_field = data_field

        #data = data.copy()
        ret = []
        for ix in data.index:
            season = []
            for c, f in enumerate(self.fields, start=0):
                if self.seasons[c] is None:
                    season.append(data[f][ix])
                else:
                    a = data[f][ix]
                    season.append(a // self.seasons[c])
        return ret

    def get_season_by_index(self,index):
        raise Exception("Operation not available!")

        for season in indexes:
                    data = data[data[f]== season[c]]
                else:
                    data = data[(data[f] // self.seasons[c]) == season[c]]
        return data[self.data_field]

        return data[self.data_field].tolist()

    def set_data(self, data, value):
        data.loc[:,self.data_field] = value

class DateTimeSeasonalIndexer(SeasonalIndexer):
    """Use a Pandas.DataFrame date field to index the seasonality """
    def __init__(self,date_field, index_fields, index_seasons, data_field,**kwargs):
        """

        :param date_field: DataFrame field that contains the datetime field used on index
        :param index_fields: List with commom.DataTime fields
        :param index_seasons: Multiples of index_fields, the default is 1
        :param data_field: DataFrame field with the time series data
        """
        super(DateTimeSeasonalIndexer, self).__init__(len(index_seasons), **kwargs)
        self.date_field = date_field

    def get_season_of_data(self, data):

        ret = []

            for ix in data.index:
                date = data[self.date_field][ix]
                season = []
                for c, f in enumerate(self.fields, start=0):
                    tmp = common.strip_datepart(date, f)
                    if self.seasons[c] is not None:
                        tmp = tmp // self.seasons[c]
                    season.append(tmp)

        elif isinstance(data, pd.Series):
            date = data[self.date_field]
            season = []
                season.append(common.strip_datepart(date, f, self.seasons[c]))

        return ret

        return data[self.date_field].tolist() if isinstance(data, pd.DataFrame) else data[self.date_field]

from enum import Enum
from pyFTS.partitioners import partitioner, Grid
from datetime import date as dt, datetime as dtm

class DateTime(Enum):
    """
    Data and Time granularity for time granularity and seasonality identification
    """
    year = 1
    half = 2        # six months
    third = 3       # four months
    quarter = 4     # three months
    sixth = 6       # two months
    month = 12
    day_of_month = 30
    day_of_year = 364
    day_of_week = 7
    hour = 24
    minute = 60
    second = 60
    hour_of_day = 24
    hour_of_week = 168
    hour_of_month = 744
    hour_of_year = 8736
    minute_of_hour = 60
    minute_of_day = 1440
    minute_of_week = 10080
    minute_of_month = 44640
    minute_of_year = 524160
    second_of_minute = 60.00001
    second_of_hour = 3600
    second_of_day = 86400

def strip_datepart(date, date_part, mask=''):
    if isinstance(date, str):
        date = dtm.strptime(date, mask)
    if date_part == DateTime.year:
        tmp = date.year
    elif date_part == DateTime.month:
        tmp = date.month
    elif date_part in (DateTime.half, DateTime.third, DateTime.quarter, DateTime.sixth):
        tmp = (date.month // date_part.value) + 1
    elif date_part == DateTime.day_of_year:
        tmp = date.timetuple().tm_yday
    elif date_part == DateTime.day_of_month:
        tmp = date.day
    elif date_part == DateTime.day_of_week:
        tmp = date.weekday()
    elif date_part == DateTime.hour or date_part == DateTime.hour_of_day:
        tmp = date.hour
    elif date_part == DateTime.hour_of_week:
        wk = (date.weekday()-1) * 24
        tmp = date.hour + wk
    elif date_part == DateTime.hour_of_month:
        wk = (date.day-1) * 24
    elif date_part == DateTime.hour_of_year:
        wk = (date.timetuple().tm_yday-1) * 24
    elif date_part == DateTime.minute or date_part == DateTime.minute_of_hour:
        tmp = date.minute
    elif date_part == DateTime.minute_of_day:
        wk = date.hour * 60
        tmp = date.minute + wk
    elif date_part == DateTime.minute_of_week:
        wk1 = (date.weekday()-1) * 1440 #24 * 60
        wk2 = date.hour * 60
        tmp = date.minute + wk1 + wk2
    elif date_part == DateTime.minute_of_month:
        wk1 = (date.day - 1) * 1440 #24 * 60
    elif date_part == DateTime.minute_of_year:
        wk1 = (date.timetuple().tm_yday - 1) * 1440 #24 * 60
    elif date_part == DateTime.second or date_part == DateTime.second_of_minute:
        tmp = date.second
    elif date_part == DateTime.second_of_hour:
        wk1 = date.minute * 60
        tmp = date.second + wk1
    elif date_part == DateTime.second_of_day:
        wk1 = date.hour * 3600 #60 * 60
        wk2 = date.minute * 60
        tmp = date.second + wk1 + wk2
    else:
        raise Exception("Unknown DateTime value!")

    return tmp

    """
    Temporal/Seasonal Fuzzy Set
    """

    def __init__(self, datepart, name, mf, parameters, centroid, alpha=1.0, **kwargs):
        super(FuzzySet, self).__init__(name, mf, parameters, centroid, alpha,
                                       **kwargs)
        self.datepart = datepart
        self.type = kwargs.get('type', 'seasonal')

        if self.type == 'seasonal' and isinstance(x, (dt, pd.Timestamp)):
            dp = strip_datepart(x, self.datepart)
        else:
            dp = x

        return dp

from pyFTS.common import Membership, FuzzySet as FS
from pyFTS.common.Composite import FuzzySet as Composite
from pyFTS.models.seasonal.common import DateTime, FuzzySet, strip_datepart
import matplotlib.pylab as plt

class TimeGridPartitioner(partitioner.Partitioner):
    """Even Length DateTime Grid Partitioner"""

        """
        Even Length Grid Partitioner
        :param seasonality: Time granularity, from pyFTS.models.seasonal.common.DateTime
        :param data: Training data of which the universe of discourse will be extracted. The universe of discourse is the open interval between the minimum and maximum values of the training data.
        :param npart: The number of universe of discourse partitions, i.e., the number of fuzzy sets that will be created
        :param func: Fuzzy membership function (pyFTS.common.Membership)
        """
        super(TimeGridPartitioner, self).__init__(name="TimeGrid", preprocess=False, **kwargs)

        self.season = kwargs.get('seasonality', DateTime.day_of_year)
        '''Seasonality, a pyFTS.models.seasonal.common.DateTime object'''
        self.mask = kwargs.get('mask', '%Y-%m-%d %H:%M:%S')
        '''A string with datetime formating mask'''

        if self.season == DateTime.year:
            ndata = [strip_datepart(k, self.season) for k in data]
            self.min = min(ndata)
            self.max = max(ndata)
        else:
            tmp = (self.season.value / self.partitions) / 2
            self.min = tmp
            self.max = self.season.value + tmp

        self.type = kwargs.get('type','seasonal')

        self.sets = self.build(None)

            self.ordered_sets = self.setnames
        else:
            self.ordered_sets = FS.set_ordered(self.sets)

    def extractor(self,x):
        if self.type == 'seasonal':
            return strip_datepart(x, self.season, self.mask)
        else:
            return x

        sets = {}

        kwargs = {'variable': self.variable, 'type': self.type }

            dlen = (self.max - self.min)
            partlen = dlen / self.partitions
        elif self.season == DateTime.day_of_week:
            self.min, self.max, partlen, pl2 = 0, 7, 1, 1
        elif self.season == DateTime.minute:
            self.min, self.max, partlen, pl2 = 0, 60, 1, 1
        elif self.season == DateTime.hour:
            self.min, self.max, partlen, pl2 = 0, 24, 1, 1
        elif self.season == DateTime.month:
            self.min, self.max, partlen, pl2 = 1, 13, 1, 1
        elif self.season  in (DateTime.half, DateTime.third, DateTime.quarter, DateTime.sixth):
            self.min, self.max, partlen, pl2 = 1, self.season.value+1, 1, 1
        else:
            partlen = self.season.value / self.partitions
            pl2 = partlen / 2

        for count, midpoint in enumerate(np.arange(self.min, self.max, partlen)):
            set_name = self.get_name(count)
            if self.membership_function == Membership.trimf:
                if midpoint == self.min or count == 0:
                    tmp = Composite(set_name, superset=True, **kwargs)
                    tmp.append_set(FuzzySet(self.season, set_name, Membership.trimf,
                                            [self.season.value - pl2, self.season.value,
                                             self.season.value + pl2], self.season.value, alpha=1,
                                            **kwargs))
                                            [midpoint - partlen, midpoint, midpoint + partlen], midpoint,
                    tmp.centroid = midpoint
                    sets[set_name] = tmp
                elif midpoint == self.max - partlen or count == self.partitions - 1:
                                            [-pl2, 0.0,
                                             pl2], 0.0, alpha=1,
                else:
                    sets[set_name] = FuzzySet(self.season, set_name, Membership.trimf,
                                         [midpoint - partlen, midpoint, midpoint + partlen], midpoint,
                                         **kwargs)
            elif self.membership_function == Membership.gaussmf:
                sets[set_name] = FuzzySet(self.season, set_name, Membership.gaussmf, [midpoint, partlen / 3], midpoint,
                                     **kwargs)
            elif self.membership_function == Membership.trapmf:
                q = partlen / 4
                if midpoint == self.min:
                    tmp = Composite(set_name, superset=True)
                                             self.season.value + 0.0000001], 0,
                    tmp.append_set(FuzzySet(self.season, set_name, Membership.trapmf,
                                            [midpoint - partlen, midpoint - q, midpoint + q, midpoint + partlen], midpoint,
                else:
                    sets[set_name] = FuzzySet(self.season, set_name, Membership.trapmf,
                                         [midpoint - partlen, midpoint - q, midpoint + q, midpoint + partlen], midpoint,
                                         **kwargs)
            count += 1

        self.min = 0

        return sets

        points = []

        fset = self.sets[self.ordered_sets[0]]
        points.append([fset.sets[1].lower, fset.sets[1].centroid, fset.sets[1].upper])

        for ct, key in enumerate(self.ordered_sets[1:-1]):
            fset = self.sets[key]
            points.append([fset.lower, fset.centroid, fset.upper])

        fset = self.sets[self.ordered_sets[-1]]

        import sys

        self.kdtree = KDTree(points)

        '''

        :param results: the number of nearest fuzzy sets to return
        '''

        type = kwargs.get('type','index')
        results = kwargs.get('results',3)

        _, ix = self.kdtree.query([data, data, data], results)

        ix = ix.tolist()

        if 0 in ix:
            ix.insert(0, self.partitions-1)
        elif self.partitions-1 in ix:
            ix.insert(0, 0)

            return [self.ordered_sets[k] for k in sorted(ix)]
        else:

    def plot(self, ax):
        """
        Plot the
        :param ax:
        :return:
        """
        ax.set_title(self.name)
        ax.set_ylim([0, 1])
        ax.set_xlim([0, self.season.value])
        ticks = []
        x = []
        for key in self.sets.keys():
            s = self.sets[key]
            if s.type == 'composite':
                for ss in s.sets:
                    self.plot_set(ax, ss)
            else:
                self.plot_set(ax, s)
            ticks.append(str(round(s.centroid, 0)) + '\n' + s.name)
            x.append(s.centroid)
        ax.xaxis.set_ticklabels(ticks)
        ax.xaxis.set_ticks(x)

class Partitioner(object):
    """
    Universe of Discourse partitioner. Split data on several fuzzy sets
    """

        """
        Universe of Discourse partitioner scheme. Split data on several fuzzy sets
        """
        self.name : str = kwargs.get('name',"")
        """partitioner name"""
        self.partitions : int = kwargs.get('npart', 10)
        """The number of universe of discourse partitions, i.e., the number of fuzzy sets that will be created"""
        self.sets : dict = {}
        """The fuzzy sets dictionary"""
        self.membership_function = kwargs.get('func', Membership.trimf)
        """Fuzzy membership function (pyFTS.common.Membership)"""
        self.setnames : list = kwargs.get('names', None)
        """list of partitions names. If None is given the partitions will be auto named with prefix"""
        self.prefix : str = kwargs.get('prefix', 'A')
        """prefix of auto generated partition names"""
        self.transformation = kwargs.get('transformation', None)
        """data transformation to be applied on data"""
        self.indexer = kwargs.get('indexer', None)
        """In a multivariate context, the variable that contains this partitioner"""
        """The type of fuzzy sets that are generated by this partitioner"""
        self.ordered_sets : list = None
        """A ordered list of the fuzzy sets names, sorted by their middle point"""
        self.kdtree : KDTree = None
        """A spatial index to help in fuzzyfication"""
        self.margin : float = kwargs.get("margin", 0.1)
        """The upper and lower exceeding margins for the known UoD. The default value is .1"""
        self.lower_margin : float = kwargs.get("lower_margin", self.margin)
        """Specific lower exceeding margins for the known UoD. The default value is the self.margin parameter"""
        self.upper_margin : float = kwargs.get("lower_margin", self.margin)
        """Specific upper exceeding margins for the known UoD. The default value is the self.margin parameter"""

        if kwargs.get('preprocess',True):

            data = kwargs.get('data',[None])

            if self.indexer is not None:
                ndata = self.indexer.get_data(data)
            else:
                ndata = data

            if self.transformation is not None:
                ndata = self.transformation.apply(ndata)
            else:

                ndata = self.indexer.get_data(ndata)

            _min = np.nanmin(ndata)
            if _min == -np.inf:
                ndata[ndata == -np.inf] = 0
                _min = np.nanmin(ndata)

            self.min = float(_min * (1 + self.lower_margin) if _min < 0 else _min * (1 - self.lower_margin))

            _max = np.nanmax(ndata)
            self.max = float(_max * (1 + self.upper_margin) if _max > 0 else _max * (1 - self.upper_margin))

            self.sets = self.build(ndata)

            self.partitions = len(self.sets)

            if self.ordered_sets is None and self.setnames is not None:
                self.ordered_sets = self.setnames[:len(self.sets)]
            else:
                self.ordered_sets = FuzzySet.set_ordered(self.sets)

            del(ndata)

        """Extract a single primitive type from an structured instance"""
        return x

        """
        Perform the partitioning of the Universe of Discourse

        :param data:  training data
        :return: 
        """
        pass

    def get_name(self, counter):
        """
        Find the name of the fuzzy set given its counter id.

        :param counter: The number of the fuzzy set
        :return: String
        """
        return self.prefix + str(counter) if self.setnames is None else self.setnames[counter]

    def lower_set(self):
        """
        Return the fuzzy set on lower bound of the universe of discourse.

        :return: Fuzzy Set
        """
        return self.sets[self.ordered_sets[0]]

    def upper_set(self):
        """
        Return the fuzzy set on upper bound of the universe of discourse.

        """
        return self.sets[self.ordered_sets[-1]]

        points = []

        #self.index = {}

            #self.index[ct] = fset.name

        import sys

        """
        Fuzzyfy the input data according to this partitioner fuzzy sets.

        :param data: input value to be fuzzyfied
        :keyword alpha_cut: the minimal membership value to be considered on fuzzyfication (only for mode='sets')
        :keyword method: the fuzzyfication method (fuzzy: all fuzzy memberships, maximum: only the maximum membership)
        :keyword mode: the fuzzyfication mode (sets: return the fuzzy sets names, vector: return a vector with the membership
        values for all fuzzy sets, both: return a list with tuples (fuzzy set, membership value) )

        :returns a list with the fuzzyfied values, depending on the mode
        """

        if isinstance(data, (tuple, list, np.ndarray)):
            ret = []
            for inst in data:
            return ret

        method = kwargs.get('method', 'fuzzy')

        nearest = self.search(data, type='index')
        mv = np.zeros(self.partitions)

        for ix in nearest:
            tmp = self[ix].membership(data)
            mv[ix] = tmp if tmp >= alpha_cut else 0.

        ix = np.ravel(np.argwhere(mv > 0.))

        if ix.size == 0:
            mv[self.check_bounds(data)] = 1.

        if method == 'fuzzy' and mode == 'vector':
            return mv
        elif method == 'fuzzy' and mode == 'sets':
            try:
                ix = np.ravel(np.argwhere(mv > 0.))
                sets = [self.ordered_sets[i] for i in ix if i < self.partitions]
                return sets
            except Exception as ex:
                return None
        elif method == 'maximum' and mode == 'sets':
            mx = max(mv)
            ix = np.ravel(np.argwhere(mv == mx))
            return self.ordered_sets[ix[0]]
            ix = np.ravel(np.argwhere(mv > 0.))
            sets = [(self.ordered_sets[i], mv[i]) for i in ix]
            return sets

        num = []
        den = []
            fset = val[0]
            mv = val[1]
                num.append( self.sets[fset].centroid * mv )
                den.append(mv)
                num.append(self.sets[fset].centroid)
                num.append(self.sets[self.ordered_sets[fset]].centroid * mv)
            else:

        if mode in ('both','vector'):
            return np.nansum(num) / np.nansum(den)
        else:
            return np.nanmean(num)

    def check_bounds(self, data):
        """
        Check if the input data is outside the known Universe of Discourse and, if it is, round it to the closest
        fuzzy set.

        :param data: input data to be verified
        :return: the index of the closest fuzzy set when data is outside de universe of discourse or None if
        the data is inside the UoD.
        """

        if data < self.min:
            return 0
        elif data > self.max:
            return self.partitions-1

        """

        """

        results = kwargs.get('results', 3)

        else:

    def plot(self, ax, rounding=0):
        """
        Plot the partitioning using the Matplotlib axis ax

        :param ax: Matplotlib axis
        """
        ax.set_ylim([0, 1.1])
        ax.set_xlim([self.min, self.max])
        ticks = []
        x = []
            if s.type == 'common':
            elif s.type == 'composite':
            ticks.append(str(round(s.centroid,rounding))+'\n'+s.name)

    def plot_set(self, ax, s):
        """
        Plot an isolate fuzzy set on Matplotlib axis

        :param s: Fuzzy Set
        """
        if s.mf == Membership.trimf:
            ax.plot([s.parameters[0], s.parameters[1], s.parameters[2]], [0, s.alpha, 0])
        elif s.mf in (Membership.gaussmf, Membership.bellmf, Membership.sigmf):
            tmpx = np.linspace(s.lower, s.upper, 100)
            tmpy = [s.membership(kk) for kk in tmpx]
            ax.plot(tmpx, tmpy)
        elif s.mf == Membership.trapmf:
            ax.plot(s.parameters, [0, s.alpha, s.alpha, 0])
        elif s.mf == Membership.singleton:
            ax.plot([s.parameters[0],s.parameters[0]], [0, s.alpha])

        """
        Return a string representation of the partitioner, the list of fuzzy sets and their parameters

        :return:
        """
        tmp = self.name + ":\n"
            tmp += str(self.sets[key])+ "\n"
        return tmp

        """
        Return the number of partitions

        :return: number of partitions
        """
        return self.partitions

    def __getitem__(self, item):
        """
        Return a fuzzy set by its order or its name.

        :param item: If item is an integer then it represents the fuzzy set index (order), if it was a string then
        it represents the fuzzy set name.
        :return: the fuzzy set
        """
        if isinstance(item, (int, np.int8, np.int16, np.int32, np.int64)): 
            if item < 0 or item >= self.partitions:
                raise ValueError("The fuzzy set index must be between 0 and {}.".format(self.partitions))
            return self.sets[self.ordered_sets[item]]
        elif isinstance(item, str):
            if item not in self.sets:
                raise ValueError("The fuzzy set with name {} does not exist.".format(item))
            return self.sets[item]
        else:
            raise ValueError("The parameter 'item' must be an integer or a string and the value informed was {} of type {}!".format(item, type(item)))

        """
        Iterate over the fuzzy sets, ordered by its midpoints.

        :return: An iterator over the fuzzy sets.
        """
        for key in self.ordered_sets:
            yield self.sets[key]

"""Class Partitioner with Singleton Fuzzy Sets"""

import random as rnd
import functools, operator

class ClassPartitioner(partitioner.Partitioner):
  """Class Partitioner: Given a dictionary with class/values pairs, create singleton fuzzy sets for each class"""

  def __init__(self, **kwargs):
    """
    Class Partitioner
    """
    super(ClassPartitioner, self).__init__(name="Class", preprocess = False)

    self.ordered_sets = []

    self.min = 0
    self.max = 0
    self.partitions = 0

    classes = kwargs.get("classes", {})

    for k,v in classes.items():
      self.min = min([self.min, v])
      self.max = max([self.max, v])
      self.partitions += 1
      self.sets[k] = FuzzySet.FuzzySet(k, Membership.singleton, [v], v, **kwargs)
      self.ordered_sets.append(k)

  def build(self, data : list):
    pass

def distance(x, y):
    if isinstance(x, list):
        tmp = functools.reduce(operator.add, [(x[k] - y[k]) ** 2 for k in range(0, len(x))])
    else:
        tmp = (x - y) ** 2
    return math.sqrt(tmp)

def c_means(k, dados, tam):
    # Inicializa as centróides escolhendo elementos aleatórios dos conjuntos
    centroides = [dados[rnd.randint(0, len(dados)-1)] for kk in range(0, k)]

    grupos = [-1 for x in range(0, len(dados))]

    it_semmodificacao = 0

    # para cada instância
    iteracoes = 0
    while iteracoes < 1000 and it_semmodificacao < 10:
        inst_count = 0

        modificacao = False

        for instancia in dados:

            # verifica a distância para cada centroide
            grupo_count = 0
            dist = 10000

            grupotmp = grupos[inst_count]

            for grupo in centroides:
                tmp = distance(instancia, grupo)
                if tmp < dist:
                    dist = tmp
                    # associa a a centroide de menor distância à instância
                    grupos[inst_count] = grupo_count
                grupo_count = grupo_count + 1

            if grupotmp != grupos[inst_count]:
                modificacao = True

            inst_count = inst_count + 1

        if not modificacao:
            it_semmodificacao = it_semmodificacao + 1
        else:
            it_semmodificacao = 0

        # atualiza cada centroide com base nos valores médios de todas as instâncias à ela associadas
        grupo_count = 0
        for grupo in centroides:
            total_inst = functools.reduce(operator.add, [1 for xx in grupos if xx == grupo_count], 0)
            if total_inst > 0:
                if tam > 1:
                    for count in range(0, tam):
                        soma = functools.reduce(operator.add,
                                                [dados[kk][count] for kk in range(0, len(dados)) if
                                                 grupos[kk] == grupo_count])
                        centroides[grupo_count][count] = soma / total_inst
                else:
                    soma = functools.reduce(operator.add,
                                            [dados[kk] for kk in range(0, len(dados)) if grupos[kk] == grupo_count])
                    centroides[grupo_count] = soma / total_inst
            grupo_count = grupo_count + 1

        iteracoes = iteracoes + 1

    return centroides

class CMeansPartitioner(partitioner.Partitioner):
        super(CMeansPartitioner, self).__init__(name="CMeans", **kwargs)

        sets = {}

        kwargs = {'type': self.type, 'variable': self.variable}

        centroides = c_means(self.partitions, data, 1)
        centroides.append(self.max)
        centroides.append(self.min)
        centroides = list(set(centroides))
        centroides.sort()
        for c in np.arange(1, len(centroides) - 1):
            _name = self.get_name(c)
            sets[_name] = FuzzySet.FuzzySet(_name, Membership.trimf,
                                 [round(centroides[c - 1], 3), round(centroides[c], 3), round(centroides[c + 1], 3)],
                                 round(centroides[c], 3), **kwargs)

"""
C. H. Cheng, R. J. Chang, and C. A. Yeh, “Entropy-based and trapezoidal fuzzification-based fuzzy time series approach for forecasting IT project cost,”
Technol. Forecast. Social Change, vol. 73, no. 5, pp. 524–542, Jun. 2006.
"""

def splitBelow(data,threshold):
    return [k for k in data if k <= threshold]

def splitAbove(data,threshold):
    return [k for k in data if k > threshold]

def PMF(data, threshold):
    a = sum([1.0 for k in splitBelow(data,threshold)])
    b = sum([1.0 for k in splitAbove(data, threshold)])
    l = len(data)
    return [a / l, b / l]

def entropy(data, threshold):
    pmf = PMF(data, threshold)
    if pmf[0] == 0 or pmf[1] == 0:
        return 1
    else:
        return - sum([pmf[0] * math.log(pmf[0]), pmf[1] * math.log(pmf[1])])

def informationGain(data, thres1, thres2):
    return entropy(data, thres1) - entropy(data, thres2)

def bestSplit(data, npart):
    if len(data) < 2:
        return []
    count = 1
    ndata = list(set(np.array(data).flatten()))
    ndata.sort()
    l = len(ndata)
    threshold = 0
    try:
        while count < l and informationGain(data, ndata[count - 1], ndata[count]) <= 0:
            threshold = ndata[count]
            count += 1
    except IndexError:
        print(threshold)
        print (ndata)
        print (count)

    rem = npart % 2

    if (npart - rem)/2 > 1:
        p1 = splitBelow(data,threshold)
        p2 = splitAbove(data,threshold)

        if len(p1) > len(p2):
            np1 = (npart - rem)/2 + rem
            np2 = (npart - rem)/2
        else:
            np1 = (npart - rem) / 2
            np2 = (npart - rem) / 2 + rem

        tmp = [threshold]

        for k in bestSplit(p1, np1 ): tmp.append(k)
        for k in bestSplit(p2, np2 ): tmp.append(k)

        return tmp

    else:
        return [threshold]

class EntropyPartitioner(partitioner.Partitioner):
    """Huarng Entropy Partitioner"""
        super(EntropyPartitioner, self).__init__(name="Entropy", **kwargs)

        sets = {}

        partitions = bestSplit(data, self.partitions)
        partitions.append(self.min)
        partitions.append(self.max)
        partitions = list(set(partitions))
        partitions.sort()
        for c in np.arange(1, len(partitions)-1):
            _name = self.get_name(c-1)
                sets[_name] = FuzzySet.FuzzySet(_name, Membership.trimf,
                                              [partitions[c - 1], partitions[c], partitions[c + 1]],partitions[c], **kwargs)
                b1 = (partitions[c] - partitions[c - 1])/2
                b2 = (partitions[c + 1] - partitions[c]) / 2
                sets[_name] = FuzzySet.FuzzySet(_name, Membership.trapmf,
                                              [partitions[c - 1], partitions[c] - b1,
                                               partitions[c] + b2, partitions[c + 1]],
                                              partitions[c], **kwargs)

"""
S. T. Li, Y. C. Cheng, and S. Y. Lin, “A FCM-based deterministic forecasting model for fuzzy time series,”
Comput. Math. Appl., vol. 56, no. 12, pp. 3052–3063, Dec. 2008. DOI: 10.1016/j.camwa.2008.07.033.
"""

def fuzzy_distance(x, y):
    if isinstance(x, (list, tuple, np.ndarray)):
    else:

def membership(val, vals):
    soma = 0
    for k in vals:
        if k == 0:
            k = 1
        soma = soma + (val / k) ** 2

    return soma

def fuzzy_cmeans(k, data, size, m, deltadist=0.001):
    data_length = len(data)

    # Centroid initialization
    centroids = [data[rnd.randint(0, data_length - 1)] for kk in range(0, k)]

    # Membership table
    membership_table = np.zeros((k, data_length)) #[[0 for kk in range(0, k)] for xx in range(0, data_length)]

    mean_change = 1000

    m_exp = 1 / (m - 1)

    iterations = 0

    while iterations < 1000 and mean_change > deltadist:

        mean_change = 0
        for instance in data:

            dist_groups = np.zeros(k) #[0 for xx in range(0, k)]

            for group_count, group in enumerate(centroids):
                dist_groups[group_count] = fuzzy_distance(group, instance)

            dist_groups_total = functools.reduce(operator.add, [xk for xk in dist_groups])

            for grp in range(0, k):
                if dist_groups[grp] == 0:
                    membership_table[inst_count][grp] = 1
                else:
                    membership_table[inst_count][grp] = 1 / membership(dist_groups[grp], dist_groups)
                    # membership_table[inst_count][grp] = 1/(dist_groups[grp] / dist_grupos_total)
                    # membership_table[inst_count][grp] = (1/(dist_groups[grp]**2))**m_exp / (1/(dist_grupos_total**2))**m_exp

        for group_count, group in enumerate(centroids):
            if size > 1:
                oldgrp = [xx for xx in group]
                for atr in range(0, size):
                                            [membership_table[xk][group_count] * data[xk][atr] for xk in range(0, data_length)])
                    norm = functools.reduce(operator.add, [membership_table[xk][group_count] for xk in range(0, data_length)])
                    centroids[group_count][atr] = soma / norm
            else:
                oldgrp = group
                soma = functools.reduce(operator.add,
                                        [membership_table[xk][group_count] * data[xk] for xk in range(0, data_length)])
                norm = functools.reduce(operator.add, [membership_table[xk][group_count] for xk in range(0, data_length)])
                centroids[group_count] = soma / norm

            mean_change = mean_change + fuzzy_distance(oldgrp, group)

        mean_change = mean_change / k
        iterations = iterations + 1

    return centroids

class FCMPartitioner(partitioner.Partitioner):
    """
    
    """

        super(FCMPartitioner, self).__init__(name="FCM", **kwargs)

        sets = {}

        centroids = fuzzy_cmeans(self.partitions, data, 1, 2)
        centroids.append(self.max)
        centroids.append(self.min)
        centroids = list(set(centroids))
        centroids.sort()
        for c in np.arange(1, len(centroids) - 1):
                                                [round(centroids[c - 1], 3), round(centroids[c], 3),
                                                 round(centroids[c + 1], 3)],
                                                round(centroids[c], 3), **kwargs)
                q1 = (round(centroids[c], 3) - round(centroids[c - 1], 3)) / 2
                q2 = (round(centroids[c + 1], 3) - round(centroids[c], 3)) / 2
                                                [round(centroids[c - 1], 3), round(centroids[c], 3) - q1,
                                                 round(centroids[c], 3) + q2, round(centroids[c + 1], 3)],

"""Even Length Grid Partitioner"""

class GridPartitioner(partitioner.Partitioner):
    """Even Length Grid Partitioner"""

        """
        """
        super(GridPartitioner, self).__init__(name="Grid", **kwargs)

        sets = {}

        #dlen = self.max - self.min
        #partlen = dlen / self.partitions

        count = 0
        centers, partlen = np.linspace(self.min, self.max, self.partitions, retstep=True)
        for c in centers:
            _name = self.get_name(count)
                sets[_name] = FuzzySet.FuzzySet(_name, Membership.trimf, [c - partlen, c, c + partlen],c,**kwargs)
                sets[_name] = FuzzySet.FuzzySet(_name, Membership.gaussmf, [c, partlen / 3], c,**kwargs)
                q = partlen / 2
                sets[_name] = FuzzySet.FuzzySet(_name, Membership.trapmf, [c - partlen, c - q, c + q, c + partlen], c,**kwargs)
            count += 1

        self.min = self.min - partlen

class PreFixedGridPartitioner(GridPartitioner):
    """Prefixed UoD with Even Length Grid Partitioner"""

        """
        Fixed UoD with Even Length Grid Partitioner
        """

        kwargs['preprocess'] = False

        self.max = kwargs.get('max', None)
        self.min = kwargs.get('min', None)

        if self.max is None or self.min is None:
            raise Exception("It is mandatory to inform the max and min parameters!")

        self.partitions = len(self.sets)

            self.ordered_sets = self.setnames[:len(self.sets)]
        else:
            self.ordered_sets = FuzzySet.set_ordered(self.sets)

class SingletonPartitioner(partitioner.Partitioner):
    """Singleton Partitioner: Create singleton fuzzy sets for each distinct value in UoD"""

        """
        Singleton Partitioner
        """
        super(SingletonPartitioner, self).__init__(name="Singleton", **kwargs)

    def build(self, data : list):
        sets = {}

        for count, instance in enumerate(set(data)):
            sets[_name] = FuzzySet.FuzzySet(_name, Membership.singleton, [instance], instance, **kwargs)

"""Simple Partitioner for manually informed fuzzy sets"""

class SimplePartitioner(partitioner.Partitioner):
    """Simple Partitioner for manually informed fuzzy sets"""

        """
        Simple Partitioner - the fuzzy sets are informed manually
        """

        super(SimplePartitioner, self).__init__(name="Simple", **kwargs)

        self.partitions = 0

    def append_complex(self, fs):
        self.sets[fs.name] = fs
        self.partitions += 1

        self.ordered_sets = [key for key in sorted(self.sets.keys(), key=lambda k: self.sets[k].centroid)]

        self.min = self.sets[self.ordered_sets[0]].lower
        self.max = self.sets[self.ordered_sets[-1]].upper

    def append(self, name, mf, parameters, **kwargs):
        """
        Append a new partition (fuzzy set) to the partitioner

        :param name: Fuzzy set name
        :param mf: One of the pyFTS.common.Membership functions
        :param parameters: A list with the parameters for the membership function
        :param kwargs: Optional arguments for the fuzzy set
        """
        if name is None or len(name) == 0:
            raise ValueError("The name of the fuzzy set cannot be empty")

        if name in self.sets:
            raise ValueError("This name has already been used")

        if mf is None or mf not in (Membership.trimf, Membership.gaussmf,
                                    Membership.trapmf, Membership.singleton,
                                    Membership.sigmf):
            raise ValueError("The mf parameter should be one of pyFTS.common.Membership functions, not {}".format(mf))

        if mf == Membership.trimf:
            if len(parameters) != 3:
                raise ValueError("Incorrect number of parameters for the Membership.trimf")

            centroid = parameters[1]
        elif mf == Membership.gaussmf:
            if len(parameters) != 2:
                raise ValueError("Incorrect number of parameters for the Membership.gaussmf")

            centroid = parameters[0]
        elif mf == Membership.trapmf:
            if len(parameters) != 4:
                raise ValueError("Incorrect number of parameters for the Membership.trapmf")

            centroid = (parameters[1]+parameters[2])/2
        elif mf == Membership.singleton:
            if len(parameters) != 1:
                raise ValueError("Incorrect number of parameters for the Membership.singleton")

        elif mf == Membership.sigmf:
                raise ValueError("Incorrect number of parameters for the Membership.sigmf")

            centroid = parameters[1] + (parameters[1] / (2 * parameters[0]))

        self.sets[name] = FuzzySet.FuzzySet(name, mf, parameters, centroid, **kwargs)

"""
Chiu, Stephen L. "Fuzzy model identification based on cluster estimation." Journal of Intelligent & fuzzy systems 2.3 (1994): 267-278.
"""

def imax(vec):
    i = np.argmax(vec)
    return (i, vec[i])

def subclust(data, ra, rb, eps_sup, eps_inf):
    if len(data.shape) == 1:
        data = np.reshape(data, (data.shape[0], 1))

    centers = np.zeros((0, data.shape[1]))

    # Initial potentials
    alpha = 4/ra**2
    beta = 4/rb**2

    pot = np.zeros(data.shape[0])
    for i in range(data.shape[0]):
        pot[i] = np.sum(np.exp(-alpha*np.linalg.norm(data - data[i,:], axis=1)**2))

    pot_max_i, pot_max = imax(pot)

    current_pot_max = pot_max
    while current_pot_max:
        x_star = data[pot_max_i,:]
        accept = False
        if current_pot_max > eps_sup * pot_max:
            # Accept xk as a cluster center and continue
            accept = True
        elif current_pot_max <= eps_inf * pot_max:
            # Reject xk and end the clustering process
            break
        else:
            d_min = np.min(np.linalg.norm(x_star - centers))
            if d_min/ra + current_pot_max/pot_max >= 1:
                accept = True
            else:
                pot[pot_max_i] = 0
                pot_max_i, current_pot_max = imax(pot)
        if accept:
            centers = np.vstack((centers, x_star))
            # Recompute potentials
            for i in range(data.shape[0]):
                new_pot = pot[i] - current_pot_max*np.exp(-beta*np.linalg.norm(x_star - data[i,:])**2)
                new_pot = max(0, new_pot)
                pot[i] = new_pot
            pot_max_i, current_pot_max = imax(pot)
    return centers

class SubClustPartitioner(partitioner.Partitioner):
    """Subtractive Clustering Partitioner"""
        self.ra = kwargs.get('ra', 0.8)
        self.rb = kwargs.get('rb', self.ra * 1.5)
        self.eps_sup = kwargs.get('eps_sup', 0.5)
        self.eps_inf = kwargs.get('eps_inf', 0.15)
        super(SubClustPartitioner, self).__init__(name="SubClust", **kwargs)

        sets = {}

        partitions = subclust(data, self.ra, self.rb, self.epssup, self.epsinf)
        partitions = list(np.reshape(partitions, partitions.shape[0]))
        self.partitions = len(partitions)