import logging
import swisseph as swe
import sys
import traceback
from math import floor, modf

from sanskrit_data.schema import common
from sanskrit_data.schema.common import JsonObject
from scipy.optimize import brentq

from jyotisha import names
from jyotisha.custom_transliteration import revjul, tr
from jyotisha.names.init_names_auto import init_names_auto
from jyotisha.zodiac import get_planet_lon

logging.basicConfig(
    level=logging.DEBUG,
    format="%(levelname)s: %(asctime)s {%(filename)s:%(lineno)d}: %(message)s "
)

NAMES = init_names_auto()
MAX_DAYS_PER_YEAR = 366
MAX_SZ = MAX_DAYS_PER_YEAR + 6  # plus one and minus one are usually necessary
MIN_DAYS_NEXT_ECLIPSE = 25
TITHI = {'id': 'TITHI', 'arc_len': 360.0 / 30.0, 'w_moon': 1, 'w_sun': -1}
TITHI_PADA = {'id': 'TITHI_PADA', 'arc_len': 360.0 /
              120.0, 'w_moon': 1, 'w_sun': -1}
NAKSHATRAM = {'id': 'NAKSHATRAM',
              'arc_len': 360.0 / 27.0, 'w_moon': 1, 'w_sun': 0}
NAKSHATRA_PADA = {'id': 'NAKSHATRA_PADA',
                  'arc_len': 360.0 / 108.0, 'w_moon': 1, 'w_sun': 0}
RASHI = {'id': 'RASHI', 'arc_len': 360.0 / 12.0, 'w_moon': 1, 'w_sun': 0}
YOGA = {'id': 'YOGA', 'arc_len': 360.0 / 27.0, 'w_moon': 1, 'w_sun': 1}
KARANAM = {'id': 'KARANAM', 'arc_len': 360.0 / 60.0, 'w_moon': 1, 'w_sun': -1}
SOLAR_MONTH = {'id': 'SOLAR_MONTH',
               'arc_len': 360.0 / 12.0, 'w_moon': 0, 'w_sun': 1}
SOLAR_NAKSH = {'id': 'SOLAR_NAKSH',
               'arc_len': 360.0 / 27.0, 'w_moon': 0, 'w_sun': 1}
SOLAR_NAKSH_PADA = {'id': 'SOLAR_NAKSH_PADA',
                    'arc_len': 360.0 / 108.0, 'w_moon': 0, 'w_sun': 1}
TYAJYAM_SPANS_REL = [51, 25, 31, 41, 15, 22, 31, 21, 33,
                     31, 21, 19, 22, 21, 15, 15, 11, 15,
                     57, 25, 21, 11, 11, 19, 17, 25, 31]
AMRITA_SPANS_REL = [43, 49, 55, 53, 39, 36, 55, 45, 57,
                    55, 45, 43, 46, 45, 39, 39, 35, 39,
                    45, 49, 45, 35, 35, 43, 41, 49, 55]
AMRITADI_YOGA = [[None, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 0, 0, 1, 1, 2, 2, 2, 0, 1, 0, 0, 2, 1, 1, 0, 0],
                 [None, 1, 1, 2, 0, 0, 1, 0, 1, 1, 2, 1, 1, 1,
                     1, 0, 2, 1, 1, 1, 1, 2, 0, 1, 1, 2, 1, 1],
                 [None, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1, 1, 0, 1,
                     1, 1, 2, 1, 1, 0, 1, 1, 1, 1, 2, 2, 0, 1],
                 [None, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2,
                     1, 1, 1, 1, 1, 2, 0, 0, 1, 2, 1, 0, 1, 2],
                 [None, 0, 1, 2, 2, 2, 2, 0, 0, 1, 0, 1, 2, 1,
                     1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1],
                 [None, 0, 1, 1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 0,
                     1, 1, 1, 1, 2, 0, 1, 1, 2, 1, 1, 1, 1, 0],
                 [None, 1, 1, 0, 0, 1, 1, 1, 1, 2, 0, 1, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 1, 2]]
AMRITADI_YOGA_NAMES = {1: 'siddha', 0: 'amRta', 2: 'maraNa'}
for i in range(7):
    AMRITADI_YOGA[i] = [AMRITADI_YOGA_NAMES.get(
        n, n) for n in AMRITADI_YOGA[i]]

class Time(JsonObject):

    """This  class is a time class with methods for printing, conversion etc.
    """

    def __init__(self, t):
        super().__init__()
        if type(t) == float or type(t) == int:
            self.t = t
        else:
            raise(TypeError('Input to time class must be int or float!'))

    def toString(self, default_suffix='', format='hh:mm', rounding=False):
        if self.t < 0:
            logging.error('t<0! %s ' % self.t)
            logging.error(traceback.print_stack())

        msec, secs = modf(self.t * 3600)
        msec = round(msec * 1000)
        if msec == 1000:
            msec = 0
            secs += 1

        hour = secs // 3600
        secs = secs % 3600

        suffix = default_suffix
        if format[-1] == '*':
            if hour >= 24:
                suffix = '*'
        else:
            if hour >= 24:
                hour -= 24
                suffix = '(+1)'  # Default notation for times > 23:59

        minute = secs // 60
        secs = secs % 60
        second = secs

        if format in ('hh:mm', 'hh:mm*'):
            # Rounding done if 30 seconds have elapsed
            return '%02d:%02d%s' % (hour, minute + ((secs + (msec >= 500)) >= 30) * rounding, suffix)
        elif format in ('hh:mm:ss', 'hh:mm:ss*'):
            # Rounding done if 500 milliseconds have elapsed
            return '%02d:%02d:%02d%s' % (hour, minute, second + (msec >= 500) * rounding, suffix)
        elif format in ('hh:mm:ss.sss', 'hh:mm:ss.sss*'):
            return '%02d:%02d:%02d.%03d%s' % (hour, minute, second, msec, suffix)
        elif format == 'gg-pp':  # ghatika-pal
            secs = round(self.t * 3600)
            gg = secs // 1440
            secs = secs % 1440
            pp = secs // 24
            return ('%d-%d' % (gg, pp))
        elif format == 'gg-pp-vv':  # ghatika-pal-vipal
            vv_tot = round(self.t * 3600 / 0.4)
            logging.debug(vv_tot)
            vv = vv_tot % 60
            logging.debug(vv)
            vv_tot = (vv_tot - vv) // 60
            logging.debug(vv_tot)
            pp = vv_tot % 60
            logging.debug(pp)
            vv_tot = (vv_tot - pp) // 60
            logging.debug(vv_tot)
            gg = vv_tot
            logging.debug(gg)
            return ('%d-%d-%d' % (gg, pp, vv))
        else:
            raise Exception("""Unknown format""")

    def __str__(self):
        return self.toString(format='hh:mm:ss')

def get_nakshatram(jd, ayanamsha_id=swe.SIDM_LAHIRI):
    """Returns the nakshatram prevailing at a given moment

    Nakshatram is computed based on the longitude of the Moon; in
    addition, to obtain the absolute value of the longitude, the
    ayanamsa is required to be subtracted.

    Args:
      float jd, the Julian day

    Returns:
      int nakShatram, where 1 stands for Ashwini, ..., 14 stands
      for Chitra, ..., 27 stands for Revati

    Examples:
      >>> get_nakshatram(2444961.7125)
      16
    """

    return get_angam(jd, NAKSHATRAM, ayanamsha_id=ayanamsha_id)

def get_yoga(jd, ayanamsha_id=swe.SIDM_LAHIRI):
    """Returns the yoha prevailing at a given moment

    Yoga is computed based on the longitude of the Moon and longitude of
    the Sun; in addition, to obtain the absolute value of the longitude, the
    ayanamsa is required to be subtracted (for each).

    Args:
      float jd, the Julian day

    Returns:
      int yoga, where 1 stands for Vishkambha and 27 stands for Vaidhrti

    Examples:
      >>> get_yoga(2444961.7125)
      8
    """

    return get_angam(jd, YOGA, ayanamsha_id=ayanamsha_id)

def get_solar_rashi(jd, ayanamsha_id=swe.SIDM_LAHIRI):
    """Returns the solar rashi prevailing at a given moment

    Solar month is computed based on the longitude of the sun; in
    addition, to obtain the absolute value of the longitude, the
    ayanamsa is required to be subtracted.

    Args:
      float jd, the Julian day

    Returns:
      int rashi, where 1 stands for mESa, ..., 12 stands for mIna

    Examples:
      >>> get_solar_rashi(2444961.7125)
      9
    """

    return get_angam(jd, SOLAR_MONTH, ayanamsha_id=ayanamsha_id)

def get_angam_float(jd, angam_type, offset=0, ayanamsha_id=swe.SIDM_LAHIRI, debug=False):
    """Returns the angam

      Args:
        float jd: The Julian Day at which the angam is to be computed
        angam_type: One of the pre-defined constants in the panchangam
        class, such as TITHI, NAKSHATRAM, YOGA, KARANAM or SOLAR_MONTH

      Returns:
        float angam

      Examples:
        >>> get_angam_float(2444961.7125,NAKSHATRAM)
        15.967801358055189
    """
    swe.set_sid_mode(ayanamsha_id)
    w_moon = angam_type['w_moon']
    w_sun = angam_type['w_sun']
    arc_len = angam_type['arc_len']

    lcalc = 0  # computing weighted longitudes
    if debug:
        logging.debug('## get_angam_float(): jd=%f', jd)
        logging.debug("Ayanamsha: %f", swe.get_ayanamsa(jd))

    #  Get the lunar longitude, starting at the ayanaamsha point in the ecliptic.
    if w_moon != 0:
        lmoon = (swe.calc_ut(jd, swe.MOON)[0][0] - swe.get_ayanamsa(jd)) % 360
        if (debug):
            logging.debug("Moon longitude: %f",
                          swe.calc_ut(jd, swe.MOON)[0][0])
            logging.debug('## get_angam_float(): lmoon=%f', lmoon)
        lcalc += w_moon * lmoon

    #  Get the solar longitude, starting at the ayanaamsha point in the ecliptic.
    if w_sun != 0:
        lsun = (swe.calc_ut(jd, swe.SUN)[0][0] - swe.get_ayanamsa(jd)) % 360
        if(debug):
            logging.debug('## get_angam_float(): lsun=%f', lsun)
        lcalc += w_sun * lsun

    if debug:
        logging.debug('## get_angam_float(): lcalc=%f', lcalc)

    lcalc = lcalc % 360

    if debug:
        logging.debug('## get_angam_float(): lcalc %% 360=%f', lcalc)
        logging.debug("offset: %f", offset)
        logging.debug(offset + int(360.0 / arc_len))

    if offset + int(360.0 / arc_len) == 0 and lcalc < arc_len:
        # Angam 1 -- needs different treatment, because of 'discontinuity'
        return (lcalc / arc_len)
    else:
        return (lcalc / arc_len) + offset

def get_planet_next_transit(jd_start, jd_end, planet, ayanamsha_id=swe.SIDM_LAHIRI):
    """Returns the next transit of the given planet e.g. swe.JUPITER

      Args:
        float jd_start, jd_end: The Julian Days between which transits must be computed
        int planet  - e.g. swe.SUN, swe.JUPITER, ...

      Returns:
        List of tuples [(float jd_transit, int old_rashi, int new_rashi)]

      Examples:
      >>> get_planet_next_transit(2457755, 2458120, swe.JUPITER)
      [(2458008.5710764076, 6, 7)]
    """
    swe.set_sid_mode(ayanamsha_id)

    transits = []
    MIN_JUMP = 15  # Random check for a transit every 15 days!
    # Could be tweaked based on planet using a dict?

    curr_L_bracket = jd_start
    curr_R_bracket = jd_start + MIN_JUMP

    while curr_R_bracket <= jd_end:
        L_rashi = floor(get_planet_lon(curr_L_bracket, planet, offset=0,
                                       ayanamsha_id=ayanamsha_id) / 30) + 1
        R_rashi = floor(get_planet_lon(curr_R_bracket, planet, offset=0,
                                       ayanamsha_id=ayanamsha_id) / 30) + 1

        if L_rashi == R_rashi:
            curr_R_bracket += MIN_JUMP
            continue
        else:
            # We have bracketed a transit!
            if L_rashi < R_rashi:
                target = R_rashi
            else:
                # retrograde transit
                target = L_rashi
            try:
                jd_transit = brentq(get_planet_lon, curr_L_bracket, curr_R_bracket,
                                    args=(planet, (-target + 1) * 30, ayanamsha_id))
                transits += [(jd_transit, L_rashi, R_rashi)]
                curr_R_bracket += MIN_JUMP
                curr_L_bracket = jd_transit + MIN_JUMP
            except ValueError:
                sys.stderr.write('Unable to compute transit of planet;\
                                 possibly could not bracket correctly!\n')
                return (None, None, None)

    return transits

def get_angam(jd, angam_type, ayanamsha_id=swe.SIDM_LAHIRI):
    """Returns the angam prevailing at a particular time

      Args:
        float jd: The Julian Day at which the angam is to be computed
        float arc_len: The arc_len for the corresponding angam

      Returns:
        int angam

      Examples:
      >>> get_angam(2444961.7125,NAKSHATRAM)
      16

      >>> get_angam(2444961.7125,TITHI)
      28

      >>> get_angam(2444961.7125,YOGA)
      8

      >>> get_angam(2444961.7125,KARANAM)
      55
    """
    swe.set_sid_mode(ayanamsha_id)

    return int(1 + floor(get_angam_float(jd, angam_type, ayanamsha_id=ayanamsha_id)))

def get_all_angas(jd, ayanamsha_id=swe.SIDM_LAHIRI):
    anga_objects = [TITHI, TITHI_PADA, NAKSHATRAM, NAKSHATRA_PADA,
                    RASHI, SOLAR_MONTH, SOLAR_NAKSH, YOGA, KARANAM]
    angas = list(map(lambda anga_object: get_angam(
        jd=jd, angam_type=anga_object, ayanamsha_id=ayanamsha_id), anga_objects))
    anga_ids = list(map(lambda anga_obj: anga_obj["id"], anga_objects))
    return dict(list(zip(anga_ids, angas)))

def get_all_angas_x_ayanamshas(jd):
    # swe.SIDM_TRUE_REVATI leads to a segfault.
    ayanamshas = [swe.SIDM_LAHIRI, swe.SIDM_ARYABHATA, swe.SIDM_ARYABHATA_MSUN, swe.SIDM_KRISHNAMURTI, swe.SIDM_JN_BHASIN, swe.SIDM_RAMAN, swe.SIDM_SS_CITRA, swe.SIDM_SS_REVATI,
                  swe.SIDM_SURYASIDDHANTA, swe.SIDM_SURYASIDDHANTA_MSUN, swe.SIDM_USHASHASHI, swe.SIDM_YUKTESHWAR, swe.SIDM_TRUE_CITRA, names.SIDM_TRUE_MULA, names.SIDM_TRUE_PUSHYA]

    ayanamsha_names = list(
        map(lambda ayanamsha: names.get_ayanamsha_name(ayanamsha), ayanamshas))
    return dict(zip(ayanamsha_names, map(lambda ayanamsha_id: get_all_angas(jd=jd, ayanamsha_id=ayanamsha_id), ayanamshas)))

def print_angas_x_ayanamshas(jd):
    anga_x_ayanamsha = get_all_angas_x_ayanamshas(jd=jd)
    import pandas
    angas_df = pandas.DataFrame(anga_x_ayanamsha)
    print(angas_df.to_csv(sep="\t"))

def get_angam_span(jd1, jd2, angam_type, target, ayanamsha_id=swe.SIDM_LAHIRI, debug=False):
    """Computes angam spans for angams such as tithi, nakshatram, yoga
        and karanam.

        Args:
          jd1: return the first span that starts after this date
          jd2: return the first span that ends before this date
          angam_type: TITHI, NAKSHATRAM, YOGA, KARANAM, SOLAR_MONTH, SOLAR_NAKSH

        Returns:
          tuple: A tuple of start and end times that lies within jd1 and jd2
    """

    angam_start = angam_end = None

    if debug:
        logging.debug(get_angam(jd1, angam_type, ayanamsha_id=ayanamsha_id))
        logging.debug(get_angam(jd2, angam_type, ayanamsha_id=ayanamsha_id))

    num_angas = int(360.0 / angam_type['arc_len'])

    jd_bracket_L = jd1
    jd_bracket_R = jd2

    h = 0.5   # Min Step for moving

    jd_now = jd1
    while jd_now < jd2 and angam_start is None:
        angam_now = get_angam(jd_now, angam_type, ayanamsha_id=ayanamsha_id)

        if debug:
            logging.debug((jd_now, revjul(jd_now), angam_now, get_angam_float(
                jd_now, angam_type, ayanamsha_id=ayanamsha_id)))
        if angam_now < target or (target == 1 and angam_now == num_angas):
            if debug:
                logging.debug(('jd_bracket_L ', jd_now))
            jd_bracket_L = jd_now
        if angam_now == target:
            try:
                angam_start = brentq(get_angam_float, jd_bracket_L, jd_now,
                                     args=(angam_type, -target + 1, ayanamsha_id, False))
            except ValueError:
                logging.error('Unable to bracket %s->%f between jd = (%f, %f), starting with (%f, %f)' %
                              (str(angam_type), -target + 1, jd_bracket_L, jd_now, jd1, jd2))
                angam_start = None
            if debug:
                logging.debug(('angam_start', angam_start))
        # if angam_now > target and angam_start is not None:
        #     angam_end = brentq(get_angam_float, angam_start, jd_now,
        #                        args=(angam_type, -target, False))
        jd_now += h

    if angam_start is None:
        return (None, None)  # If it doesn't start, we don't care if it ends!
    jd_now = angam_start

    while jd_now < jd2 and angam_end is None:
        angam_now = get_angam(jd_now, angam_type, ayanamsha_id=ayanamsha_id)

        if debug:
            logging.debug((jd_now, revjul(jd_now), angam_now, get_angam_float(
                jd_now, angam_type, ayanamsha_id=ayanamsha_id)))
        if target == num_angas:
            # Wait till we land at the next anga!
            if angam_now == 1:
                jd_bracket_R = jd_now
                if debug:
                    logging.debug(('jd_bracket_R ', jd_now))
                break
        else:
            if angam_now > target:
                jd_bracket_R = jd_now
                if debug:
                    logging.debug(('jd_bracket_R ', jd_now))
                break
        jd_now += h

    try:
        angam_end = brentq(get_angam_float, angam_start, jd_bracket_R,
                           args=(angam_type, -target, ayanamsha_id, False))
    except ValueError:
        logging.error('Unable to compute angam_end (%s->%d); possibly could not bracket correctly!\n' %
                      (str(angam_type), target))

    if debug:
        logging.debug(('angam_end', angam_end))

    return (angam_start, angam_end)

def get_angam_data(jd_sunrise, jd_sunrise_tmrw, angam_type, ayanamsha_id=swe.SIDM_LAHIRI):
    """Computes angam data for angams such as tithi, nakshatram, yoga
    and karanam.

    Args:
      angam_type: TITHI, NAKSHATRAM, YOGA, KARANAM, SOLAR_MONTH, SOLAR_NAKSH

    Returns:
      tuple: A tuple comprising
        angam_sunrise: The angam that prevails as sunrise
        angam_data: a list of (int, float) tuples detailing the angams
        for the day and their end-times (Julian day)

    Examples:
      >>> get_angam_data(2444961.54042,2444962.54076,TITHI)
      [(27, 2444961.599213231)]
      >>> get_angam_data(2444961.54042,2444962.54076,NAKSHATRAM)
      [(16, 2444961.7487953394)]
      >>> get_angam_data(2444961.54042,2444962.54076,YOGA)
      [(8, 2444962.1861976916)]
      >>> get_angam_data(2444961.54042,2444962.54076,KARANAM)
      [(54, 2444961.599213231), (55, 2444962.15444546)]
    """
    swe.set_sid_mode(ayanamsha_id)

    w_moon = angam_type['w_moon']
    w_sun = angam_type['w_sun']
    arc_len = angam_type['arc_len']

    num_angas = int(360.0 / arc_len)

    # Compute angam details
    angam_now = get_angam(jd_sunrise, angam_type, ayanamsha_id=ayanamsha_id)
    angam_tmrw = get_angam(jd_sunrise_tmrw, angam_type,
                           ayanamsha_id=ayanamsha_id)

    angams_list = []

    num_angas_today = (angam_tmrw - angam_now) % num_angas

    if num_angas_today == 0:
        # The angam does not change until sunrise tomorrow
        return [(angam_now, None)]
    else:
        lmoon = (swe.calc_ut(jd_sunrise, swe.MOON)[
                 0][0] - swe.get_ayanamsa(jd_sunrise)) % 360

        lsun = (swe.calc_ut(jd_sunrise, swe.SUN)[
                0][0] - swe.get_ayanamsa(jd_sunrise)) % 360

        lmoon_tmrw = (swe.calc_ut(jd_sunrise_tmrw, swe.MOON)[0][0] -
                      swe.get_ayanamsa(jd_sunrise_tmrw)) % 360

        lsun_tmrw = (swe.calc_ut(jd_sunrise_tmrw, swe.SUN)[0][0] -
                     swe.get_ayanamsa(jd_sunrise_tmrw)) % 360

        for i in range(num_angas_today):
            angam_remaining = arc_len * (i + 1) - (((lmoon * w_moon +
                                                     lsun * w_sun) % 360) % arc_len)

            # First compute approximate end time by essentially assuming
            # the speed of the moon and the sun to be constant
            # throughout the day. Therefore, angam_remaining is computed
            # just based on the difference in longitudes for sun and
            # moon today and tomorrow.
            approx_end = jd_sunrise + angam_remaining / (((lmoon_tmrw - lmoon) % 360) * w_moon +
                                                         ((lsun_tmrw - lsun) % 360) * w_sun)

            # Initial guess value for the exact end time of the angam
            x0 = approx_end

            # What is the target (next) angam? It is needed to be passed
            # to get_angam_float for zero-finding. If the target angam
            # is say, 12, then we need to subtract 12 from the value
            # returned by get_angam_float, so that this function can be
            # passed as is to a zero-finding method like brentq or
            # newton. Since we have a good x0 guess, it is easy to
            # bracket the function in an interval where the function
            # changes sign. Therefore, brenth can be used, as suggested
            # in the scipy documentation.
            target = (angam_now + i - 1) % num_angas + 1

            # Approximate error in calculation of end time -- arbitrary
            # used to bracket the root, for brenth
            TDELTA = 0.05
            try:
                t_act = brentq(get_angam_float, x0 - TDELTA, x0 + TDELTA,
                               args=(angam_type, -target, ayanamsha_id, False))
            except ValueError:
                logging.warning(
                    'Unable to bracket! Using approximate t_end itself.')
                logging.warning(locals())
                t_act = approx_end
            angams_list.extend([((angam_now + i - 1) % num_angas + 1, t_act)])
    return angams_list

def get_ekadashi_name(paksha, lmonth):
    """Return the name of an ekadashi
    """
    if paksha == 'shukla':
        if lmonth == int(lmonth):
            return '%s-EkAdazI' % NAMES['SHUKLA_EKADASHI_NAMES']['hk'][lmonth]
        else:
            # adhika mAsam
            return '%s-EkAdazI' % NAMES['SHUKLA_EKADASHI_NAMES']['hk'][13]
    elif paksha == 'krishna':
        if lmonth == int(lmonth):
            return '%s-EkAdazI' % NAMES['KRISHNA_EKADASHI_NAMES']['hk'][lmonth]
        else:
            # adhika mAsam
            return '%s-EkAdazI' % NAMES['KRISHNA_EKADASHI_NAMES']['hk'][13]

def get_chandra_masa(month, NAMES, script, visarga=True):
    if visarga:
        if month == int(month):
            return NAMES['CHANDRA_MASA_NAMES'][script][month]
        else:
            return '%s-(%s)' % (NAMES['CHANDRA_MASA_NAMES'][script][int(month) + 1], tr('adhikaH', script, titled=False))
    else:
        if month == int(month):
            return NAMES['CHANDRA_MASA_NAMES'][script][month][:-1]
        else:
            return '%s-(%s)' % (NAMES['CHANDRA_MASA_NAMES'][script][int(month) + 1][:-1], tr('adhika', script, titled=False))

def get_tithi(jd, ayanamsha_id=swe.SIDM_LAHIRI):
    """Returns the tithi prevailing at a given moment

    Tithi is computed as the difference in the longitudes of the moon
    and sun at any given point of time. Therefore, even the ayanamsha
    does not matter, as it gets cancelled out.

    Args:
      float jd, the Julian day

    Returns:
      int tithi, where 1 stands for ShuklapakshaPrathama, ..., 15 stands
      for Paurnamasi, ..., 23 stands for KrishnapakshaAshtami, 30 stands
      for Amavasya

    Examples:
      >>> get_tithi(2444961.7125)
      28
    """

    return get_angam(jd, TITHI, ayanamsha_id=ayanamsha_id)

def get_kaalas(start_span, end_span, part_start, num_parts):
    """Compute kaalas in a given span with specified fractions

    Args:
      :param start_span float (jd)
      :param end_span float (jd)
      int part_start
      int num_parts

    Returns:
       tuple (start_time_jd, end_time_jd)

    Examples:

    """
    start_fraction = part_start / num_parts
    end_fraction = (part_start + 1) / num_parts

    start_time = start_span + (end_span - start_span) * start_fraction
    end_time = start_span + (end_span - start_span) * end_fraction

    return (start_time, end_time)

def sanitize_time(year_in, month_in, day_in, hour_in, minute_in, second_in):
    (year, month, day, hour, minute, second) = (
        year_in, month_in, day_in, hour_in, minute_in, second_in)
    if second >= 60:
        minute = minute + second / 60
        second = second % 60
    if minute >= 60:
        hour = hour + minute / 60
        minute = minute % 60
    if hour >= 24:
        day = day + hour / 24
        hour = hour % 24
    from calendar import monthrange
    (_, final_day) = monthrange(year, month)
    if day > final_day:
        assert day == final_day + 1, "range not supported by this function"
        day = 1
        month = month + 1
    if month >= 13:
        year = year + (month - 1) / 12
        month = ((month - 1) % 12) + 1
    return (year, month, day, hour, minute, second)

# Essential for depickling to work.
common.update_json_class_index(sys.modules[__name__])
# logging.debug(common.json_class_index)


# Copyright (c) 2011-2014 by California Institute of Technology
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the California Institute of Technology nor
#    the names of its contributors may be used to endorse or promote
#    products derived from this software without specific prior
#    written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL CALTECH
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
"""
Classes representing hybrid dynamical systems.
"""
from __future__ import absolute_import

import logging
logger = logging.getLogger(__name__)

from warnings import warn
import itertools
from pprint import pformat

import numpy as np
import polytope as pc

# inline imports:
#
# from tulip.graphics import newax, quiver

def _indent(s, n):
    s = s.split('\n')
    w = n*' '
    return w + ('\n'+w).join(s)

class LtiSysDyn(object):
    r"""Represent discrete-time continuous-state dynamics::

        s[t+1] = A*s[t] + B*u[t] + E*d[t] + K

    subject to the constraints::

        u[t] \in Uset
        d[t] \in Wset
        s[t] \in domain

    where:
        - u[t] the control input
        - d[t] the disturbance input
        - s[t] the system state

    A LtiSysDyn object contains the fields:

        - A, B, E, K, (matrices)
        - Uset, Wset, (each a C{polytope.Polytope})
        - domain (C{polytope.Polytope} or C{polytope.Region})
        - time_semantics: 'discrete' (if system is originally a discrete-time
          system) or 'sampled' (if system is sampled from a continuous-time
          system)
        - timestep: A positive real number containing the timestep (for sampled
          system)

    as defined above.

    Note
    ====
    For state-dependent bounds on the input,::
        [u[t]; s[t]] \in Uset
    can be used.

    See Also
    ========
    L{PwaSysDyn}, L{SwitchedSysDyn}, C{polytope.Polytope}
    """
    def __init__(self, A=None, B=None, E=None, K=None,
                 Uset=None,Wset=None, domain=None, time_semantics=None,
                 timestep=None):

        if Uset is None:
            warn('Uset not given to LtiSysDyn()')
        elif not isinstance(Uset, pc.Polytope):
            raise Exception('`Uset` has to be a Polytope')
        if domain is None:
            warn("Domain not given to LtiSysDyn()")
        if ((domain is not None) and
            (not (isinstance(domain, pc.Polytope) or
                isinstance(domain, pc.Region))
            )
        ):
            raise Exception('`domain` has to be a Polytope or Region')

        # check dimensions agree
        try:
            nA, mA = A.shape
        except:
            raise TypeError('A matrix must be 2d array')
        if nA != mA:
            raise ValueError('A must be square')
        if domain is not None:
            if domain.dim != mA:
                raise Exception('domain.dim != A.size[1]')

        if B is not None:
            try:
                nB, mB = B.shape
            except:
                raise TypeError('B matrix must be 2d array')
            if nA != nB:
                raise ValueError('A and B must have same number of rows')
            if Uset is not None:
                if (Uset.dim != mB) and (Uset.dim != mB + nA):
                    msg = 'Uset.dim != B.size[1]'
                    msg += ' and != B.size[1] + A.size[1]'
                    raise Exception(msg)

        if E is not None:
            try:
                nE, mE = E.shape
            except:
                raise TypeError('E matrix must be 2d array')
            if nA != nE:
                raise ValueError('A and E must have same number of rows')
            if Wset is not None:
                if Wset.dim != mE:
                    raise Exception('Wset.dim != E.size[1]')

        if K is not None:
            try:
                nK, mK = K.shape
            except:
                raise TypeError('K column vector must be 2d array')

            if nA != nK:
                raise ValueError('A and K must have same number of rows')
            if mK != 1:
                raise ValueError('K must be a column vector')

        self.A = A
        self.B = B

        if K is None:
            if len(A) != 0:
                self.K = np.zeros([mA, 1])
            else:
                self.K = K
        else:
            self.K = K.reshape(K.size,1)

        if E is None and (len(A) != 0):
            self.E = np.zeros([mA, 1])
            self.Wset = pc.Polytope()
        else:
            self.E = E
            self.Wset = Wset

        self.Uset = Uset
        self.domain = domain

        # Check that timestep and semantics are valid.
        _check_time_data(time_semantics, timestep)
        self.time_semantics = time_semantics
        self.timestep = timestep

    def __str__(self):
        n = 3
        output = 'A =\n' + _indent(str(self.A), n)
        output += '\nB =\n' + _indent(str(self.B), n)
        output += '\nE =\n' + _indent(str(self.E), n)
        output += '\nK =\n' + _indent(str(self.K), n)
        output += '\nUset =\n' + _indent(str(self.Uset), n)
        output += '\nWset =\n' + _indent(str(self.Wset), n)
        return output

    def plot(self, ax=None, color=np.random.rand(3), show_domain=True,
             res=(5, 5), **kwargs):
        try:
            from tulip.graphics import newax, quiver
        except:
            logger.error('failed to import graphics')
            warn('pyvectorized not found. No plotting.')
            return

        (x, res) = pc.grid_region(self.domain, res=res)
        n = self.A.shape[0]
        DA = self.A - np.eye(n)
        v = DA.dot(x) + self.K

        if ax is None:
            ax, fig = newax()

        if show_domain:
            self.domain.plot(ax, color)

        quiver(x, v, ax, **kwargs)

        return ax

class PwaSysDyn(object):
    """PwaSysDyn class for specifying a polytopic piecewise affine system.
    A PwaSysDyn object contains the fields:

      - C{list_subsys}: list of L{LtiSysDyn}

      - C{domain}: domain over which piecewise affine system is defined,
          type: polytope.Polytope or polytope.Region

      - C{time_semantics}: 'discrete' (if system is originally a discrete-time
       system) or 'sampled' (if system is sampled from a continuous-time
       system)

      - C{timestep}: A positive real number containing the timestep (for sampled
        systems)

    For the system to be well-defined the domains of its subsystems should be
    mutually exclusive (modulo intersections with empty interior) and cover the
    domain.

    See Also
    ========
    L{LtiSysDyn}, L{SwitchedSysDyn}, C{polytope.Polytope}
    """
    def __init__(self, list_subsys=[], domain=None, time_semantics=None,
                 timestep=None, overwrite_time=True):
        """
        @type overwrite_time: bool
        @param overwrite_time: If true, then overwrites any time data in the
                               objects in C{list_subsys} with the data in
                               C{time_semantics} and C{timestep} variables.
                               Otherwise checks that the time data of the
                               objects in C{list_subsys} are consistent with
                               C{time_semantics} and C{timestep}.
        """

        if domain is None:
            warn("Domain not given to PwaSysDyn()")

        if ((domain is not None) and
            (not (isinstance(domain, pc.Polytope) or
                isinstance(domain, pc.Region))
            )
        ):
            raise Exception("PwaSysDyn: `domain` has to be a Polytope or Region")

        if len(list_subsys) > 0:
            uncovered_dom = domain.copy()
            n = list_subsys[0].A.shape[1]  # State space dimension
            m = list_subsys[0].B.shape[1]  # Input space dimension
            p = list_subsys[0].E.shape[1]  # Disturbance space dimension
            for subsys in list_subsys:
                uncovered_dom = uncovered_dom.diff(subsys.domain)
                if (n!=subsys.A.shape[1] or m!=subsys.B.shape[1] or
                    p!=subsys.E.shape[1]):
                    raise Exception("PwaSysDyn: state, input, disturbance " +
                                    "dimensions have to be the same for all " +
                                     "subsystems")
            if not pc.is_empty(uncovered_dom):
                raise Exception("PwaSysDyn: subdomains must cover the domain")
            for x in itertools.combinations(list_subsys, 2):
                if pc.is_fulldim(x[0].domain.intersect(x[1].domain) ):
                    raise Exception("PwaSysDyn: subdomains have to be mutually"+
                        " exclusive")

        self.list_subsys = list_subsys
        self.domain = domain

        # Input time semantics
        _check_time_data(time_semantics, timestep)
        if overwrite_time:
            _push_time_data(self.list_subsys, time_semantics, timestep)
        else:
            _check_time_consistency(list_subsys, time_semantics, timestep)
        self.timestep = timestep
        self.time_semantics = time_semantics

    def __str__(self):
        s = 'Piecewise-Affine System Dynamics\n'
        s += 30 * '-' + 2*'\n'

        s += 3*' ' + 'Domain:\n\n'
        s += _indent(str(self.domain), n=6) + '\n'

        for i, sys in enumerate(self.list_subsys):
            s += 3*' ' + 'Subsystem: ' + str(i) +'\n'
            s += _indent(str(sys), n=6)
        return s

    @classmethod
    def from_lti(cls, A=[], B=[], E=[], K=[],
                 Uset=None, Wset=None,domain=None):
        lti_sys = LtiSysDyn(A,B,E,K,Uset,Wset,domain)
        return cls([lti_sys], domain)

    def plot(self, ax=None, show_domain=True, **kwargs):
        try:
            from tulip.graphics import newax
        except:
            logger.error('failed to import graphics')
            return

        if ax is None:
            ax, fig = newax()

        for subsystem in self.list_subsys:
            subsystem.plot(ax, color=np.random.rand(3),
                           show_domain=show_domain, **kwargs)
        return ax

class SwitchedSysDyn(object):
    """Represent hybrid systems switching between dynamic modes.

    A C{SwitchedSysDyn} represents a system with switching modes
    that depend on both discrete:

        - n_env environment variables (uncontrolled)
        - n_sys system variables (controlled)

    A C{SwitchedSysDyn} object contains the fields:

     - C{disc_domain_size}: 2-tuple of numbers of modes
       type: (n_env, n_sys)

     - C{env_labels}: (optional) labels for discrete environment variables
       type: list of len(n_env)
       default: range(n_env)

     - C{disc_sys_labels}: (optional) labels for discrete system variables
       type: list of len(n_sys)
       default: range(n_sys)

     - C{dynamics}: mapping mode 2-tuples to active dynamics::

         (env_label, sys_label) -> PwaSysDyn

       type: dict
       default: If no env_label or sys_label passed,
       then default to int indices (i,j) L{PwaSysDyn}.

     - C{cts_ss}: continuous state space over which hybrid system is defined.
       type: C{polytope.Region}

     - C{time_semantics}: 'discrete' (if system is originally a discrete-time
       system) or 'sampled' (if system is sampled from a continuous-time
       system)

     - C{timestep}: A positive real number containing the timestep (for sampled
       systems)

    Note
    ====
    We assume that system and environment switching modes are
    independent of one another.  (Use LTL statement to make it not so.)

    See Also
    ========
    L{LtiSysDyn}, L{PwaSysDyn}, C{polytope.Region}
    """
    def __init__(self, disc_domain_size=(1,1),
                 dynamics=None, cts_ss=None,
                 env_labels=None, disc_sys_labels=None, time_semantics=None,
                 timestep=None, overwrite_time=True):
        """
        @type overwrite_time: bool
        @param overwrite_time: If true, then overwrites any time data in the
                               objects in C{list_subsys} with the data in
                               C{time_semantics} and C{timestep} variables.
                               Otherwise checks that the time data of the
                               objects in C{list_subsys} are consistent with
                               C{time_semantics} and C{timestep}.
        """

        # check that the continuous domain is specified
        if cts_ss is None:
            warn('continuous state space not given to SwitchedSysDyn')
        else:
            if not isinstance(cts_ss, (pc.Polytope, pc.Region) ):
                raise Exception('SwitchedSysDyn: ' +
                   '`cts_ss` must be a Polytope or Region')

        self.disc_domain_size = disc_domain_size

        # If label numbers agree with disc_domain_size, then use them.
        # Otherwise, ignore the labels.
        n_env, n_sys = disc_domain_size

        self._env_labels = self._check_labels(n_env, env_labels)
        self._disc_sys_labels = self._check_labels(n_sys, disc_sys_labels)

        # Check each dynamics key is a valid mode,
        # i.e., a valid combination of env and sys labels.
        if dynamics is not None:
            modes = self.all_mode_combs

            undefined_modes = set(dynamics.keys()).difference(modes)

            if undefined_modes:
                msg = 'SwitchedSysDyn: `dynamics` keys inconsistent'
                msg += ' with discrete mode labels.\n'
                msg += 'Undefined modes:\n' + str(undefined_modes)
                raise ValueError(msg)

            missing_modes = set(modes).difference(dynamics.keys())

            if missing_modes:
                msg = 'Missing the modes:\n' + str(missing_modes)
                msg += '\n Make sure you did not forget any modes,\n'
                msg += 'otherwise this is fine.'
                warn(msg)

            if not all([isinstance(sys, PwaSysDyn)
                        for sys in dynamics.values()]):
                msg = 'For each mode dynamics must be PwaSysDyn.\n'
                msg += 'Got instead: ' +str(type(sys))
                raise Exception(msg)

        self.dynamics = dynamics
        self.cts_ss = cts_ss

        _check_time_data(time_semantics, timestep)
        if overwrite_time:
            _push_time_data(self.dynamics.values(), time_semantics, timestep)
        else:
            _check_time_consistency(list(dynamics.values()), time_semantics, timestep)
        self.timestep = timestep
        self.time_semantics = time_semantics

    def __str__(self):
        n_env, n_sys = self.disc_domain_size

        s = 'Hybrid System Dynamics\n'
        s += 30 * '-' + '\n'

        s += 'Modes:\n'
        s += 4*' ' + 'Environment (' + str(n_env) + ' modes):\n'
        s += 6*' ' + pformat(self.env_labels, indent=3) + 2*'\n'
        s += 4*' ' + 'System: (' + str(n_sys) + ' modes)\n'
        s += 6*' ' + pformat(self.disc_sys_labels, indent=3) + 2*'\n'

        s += 'Continuous State Space:\n\n'
        s += _indent(str(self.cts_ss), 4) + '\n'

        s += 'Dynamics:\n'
        for mode, pwa in self.dynamics.items():
            s += 4*' ' + 'mode: ' + str(mode) + '\n'
            s += 4*' ' + 'dynamics:\n' + _indent(str(pwa), 8) +'\n\n'
        return s

    def _check_labels(self, n, labels):
        # don't complain for default
        if labels is None:
            return None

        # len exists ?
        try:
            # is len correct ?
            if len(labels) != n:
                msg = 'number of environment labels is inconsistent'
                msg += ' with discrete domain size.\n'
                msg += 'Ignoring given environment labels.\n'
                msg += 'Defaulting to integer labels.'
                warn(msg)

                return None
        except:
            warn('Environment labels of type: ' +
                 type(labels) + 'have no len()')
            return None
        return labels

    @property
    def all_mode_combs(self):
        """Return all possible combinations of modes.
        """
        modes = [(a,b) for a in self.env_labels
                           for b in self.disc_sys_labels]

        logger.debug('Available modes: ' + str(modes) )
        return modes

    @property
    def modes(self):
        if self.dynamics is None:
            warn('No dynamics defined (None).')
            return None
        return self.dynamics.keys()

    @property
    def env_labels(self):
        if self._env_labels is None:
            return range(self.disc_domain_size[0])
        else:
            return self._env_labels

    @property
    def disc_sys_labels(self):
        if self._disc_sys_labels is None:
            return range(self.disc_domain_size[1])
        else:
            return self._disc_sys_labels

    @classmethod
    def from_pwa(cls, list_subsys=[], domain=None):
        pwa_sys = PwaSysDyn(list_subsys,domain)
        return cls((1,1), {(0,0):pwa_sys}, domain)

    @classmethod
    def from_lti(cls, A=[], B=[], E=[], K=[],
                 Uset=None, Wset=None,domain=None):
        pwa_sys = PwaSysDyn.from_lti(A, B, E, K,
                                     Uset, Wset, domain)
        return cls((1,1), {(0,0):pwa_sys}, domain)

def _push_time_data(system_list, time_semantics, timestep):
    """Overwrite the time data in system list. Throws warnings if overwriting
    existing data."""

    for system in system_list:
        if (system.time_semantics != time_semantics) and (system.time_semantics
            is not None):
            warn('Overwriting existing time semantics data.')
        if (system.timestep != timestep) and (system.timestep is not None):
            warn('Overwriting existing timestep data.')
        system.time_semantics = time_semantics
        system.timestep = timestep

        # Overwrite LTI in system if system is a PWA
        if isinstance(system, PwaSysDyn):
            _push_time_data(system.list_subsys, time_semantics, timestep)

def _check_time_data(semantics, timestep):
    """Checks that time semantics and timestep are correctly specified. Raises
    ValueErrors if that's not the case.

    @type semantics: string
    @param timestep: any positive number
    @type timestep: int or float

    @rtype: None
    """

    if semantics not in ['sampled', 'discrete', None]:
        raise ValueError('Time semantics must be discrete or ' +
            'sampled (sampled from continuous time system).')

    if ((semantics == 'discrete') and (timestep is not None)):
        raise ValueError('Discrete semantics must not have a timestep')

    if timestep is not None:
        error_string = 'Timestep must be a positive real number or unspecified.'
        if timestep <= 0:
            raise ValueError(error_string)
        if not isinstance(timestep, (int, float)):
            raise TypeError(error_string)

def _check_time_consistency(system_list, time_semantics, timestep):
    """Checks that all the dynamical systems in system_list have the same time
    semantics and timestep. Raises ValueError if not the case.

    @type system_list: list of L{LtiSysDyn} or L{PwaSysDyn}
    @rtype: None
    """

    # Check that time semantics for all subsystems match
    for ind in range(len(system_list)-1):

        if system_list[ind].timestep != system_list[ind+1].timestep:
            raise ValueError('Not all timesteps in child systems are the same.')

        if system_list[ind].time_semantics != system_list[ind+1].time_semantics:
            raise ValueError('Not all time semantics are the same.')

    # Check that time semantics for all subsystems match specified system and
    # timestep
    if system_list[0].timestep != timestep:
        raise ValueError('Timestep of subsystems do not match specified ' +
                         'timestep.')

    if system_list[0].time_semantics != time_semantics:
        raise ValueError('Time semantics of subsystems do not match ' +
                         'specified time semantics.')


"""

Source.py

Author: Jordan Mirocha
Affiliation: University of Colorado at Boulder
Created on: Sun Jul 22 16:28:08 2012

Description: Initialize a radiation source.

"""
from __future__ import print_function
import re, os
import numpy as np
from scipy.integrate import quad
from ..util import ParameterFile
from ..physics.Hydrogen import Hydrogen
from ..physics.Cosmology import Cosmology
from ..util.ParameterFile import ParameterFile
from ..static.IntegralTables import IntegralTable
from ..static.InterpolationTables import LookupTable
from ..physics.Constants import erg_per_ev, E_LL, s_per_myr
from ..util.SetDefaultParameterValues import SourceParameters, \
    CosmologyParameters
from ..physics.CrossSections import PhotoIonizationCrossSection as sigma_E

try:
    import h5py
except ImportError:
    pass

np.seterr(all='ignore')   # exp overflow occurs when integrating BB
                          # will return 0 as it should for x large

class Source(object):
    def __init__(self, grid=None, cosm=None, logN=None, init_tabs=True,
        **kwargs):
        """
        Initialize a radiation source object.

        ..note:: This is inherited by all other ares.sources classes.

        Parameters
        ----------
        grid: rt1d.static.Grid.Grid instance
        logN: column densities over which to tabulate integral quantities

        """

        self.pf = ParameterFile(**kwargs)
        self._cosm_ = cosm

        # Create lookup tables for integral quantities
        if init_tabs and (grid is not None):
            self._create_integral_table(logN=logN)

    @property
    def Emin(self):
        return self.pf['source_Emin']
    @property
    def Emax(self):
        return self.pf['source_Emax']

    @property
    def EminNorm(self):
        if not hasattr(self, '_EminNorm'):
            if self.pf['source_EminNorm'] == None:
                self._EminNorm = self.pf['source_Emin']
            else:
                self._EminNorm = self.pf['source_EminNorm']

        return self._EminNorm

    @property
    def EmaxNorm(self):
        if not hasattr(self, '_EmaxNorm'):
            if self.pf['source_EmaxNorm'] == None:
                self._EmaxNorm = self.pf['source_Emax']
            else:
                self._EmaxNorm = self.pf['source_EmaxNorm']

        return self._EmaxNorm

    @property
    def info(self):
        """
        Print info like Nlw etc in various units!
        """
        pass

    @property
    def is_delta(self):
        return self.pf['source_sed'] == 'delta'

    def SourceOn(self, t):
        if t < self.tau:
            return True
        else:
            return False

    @property
    def tau(self):
        if not hasattr(self, '_tau'):
            self._tau = self.pf['source_lifetime'] * s_per_myr
        return self._tau

    @property
    def cosm(self):
        if not hasattr(self, '_cosm'):
            if self._cosm_ is not None:
                self._cosm = self._cosm_
            elif self.grid is not None:
                self._cosm = self.grid.cosm
            else:
                self._cosm = Cosmology(pf=self.pf, **self.pf)

        return self._cosm

    @property
    def multi_freq(self):
        if not hasattr(self, '_multi_freq'):
            self._multi_freq = self.discrete and not self.pf['source_multigroup']

        return self._multi_freq

    @property
    def multi_group(self):
        if not hasattr(self, '_multi_group'):
            self._multi_group = self.discrete and self.pf['source_multigroup']

        return self._multi_group

    @property
    def ionizing(self):
        # See if source emits ionizing photons
        # Should also be function of absorbers
        if not hasattr(self, '_ionizing'):
            self._ionizing = self.pf['source_Emax'] > E_LL

        return self._ionizing

    @property
    def grid(self):
        if not hasattr(self, '_grid'):
            self._grid = None

        return self._grid

    @grid.setter
    def grid(self, value):
        self._grid = value

    @property
    def discrete(self):
        if not hasattr(self, '_discrete'):
            self._discrete = (self.pf['source_E'] != None) or \
                (self.pf['source_sed'] in ['eldridge2009', 'eldridge2017',
                    'leitherer1999'])

        return self._discrete

    @property
    def continuous(self):
        if not hasattr(self, '_continuous'):
            self._continuous = not self.discrete

        return self._continuous

    @property
    def hydr(self):
        if not hasattr(self, '_hydr'):
            self._hydr = None

        return self._hydr

    @hydr.setter
    def hydr(self, value):
        self._hydr = value

    @property
    def frec(self):
        """
        Compute average recycling fraction (i.e., spectrum-weighted frec).
        """

        if self.hydr is None:
            return None

        n = np.arange(2, self.hydr.nmax)
        En = np.array(list(map(self.hydr.ELyn, n)))
        In = np.array(list(map(self.Spectrum, En))) / En
        fr = np.array(list(map(self.hydr.frec, n)))

        return np.sum(fr * In) / np.sum(In)

    @property
    def intrinsic_hardening(self):
        if not hasattr(self, '_intrinsic_hardening'):
            if 'source_hardening' in self.pf:
                self._intrinsic_hardening = \
                    self.pf['source_hardening'] == 'intrinsic'
            else:
                self._intrinsic_hardening = False

        return self._intrinsic_hardening

    def _hardening_factor(self, E):
        return np.exp(-10.**self.logN \
            * (sigma_E(E, 0) + self.cosm.y * sigma_E(E, 1)))

    @property
    def logN(self):
        if not hasattr(self, '_logN'):
            if 'source_logN' in self.pf:
                self._logN = self.pf['source_logN']
            else:
                self._logN = -np.inf

        return self._logN

    @property
    def sharp_points(self):
        if not hasattr(self, '_sharp_points'):
            if self.pf['source_sed_sharp_at'] is not None:
                self._sharp_points = [self.pf['source_sed_sharp_at']]
            else:
                self._sharp_points = None

        return self._sharp_points

    @property
    def _normL(self):
        if not hasattr(self, '_normL_'):
            if self.is_delta:
                self._normL_ = 1. #/ self.pf['source_Emax']#/ self._Intensity(self.pf['source_Emax'])
            elif self.pf['source_Enorm'] is not None:
                En = self.pf['source_Enorm']

                if self.intrinsic_hardening:
                    self._normL_ = 1. / self._Intensity(En),
                else:
                    self._normL_ = 1. / (self._Intensity(En) / self._hardening_factor(En))
            else:
                if self.intrinsic_hardening:
                    self._normL_ = 1. / quad(self._Intensity,
                        self.pf['source_EminNorm'],
                        self.pf['source_EmaxNorm'], points=self.sharp_points)[0]
                else:
                    integrand = lambda EE: self._Intensity(EE) / self._hardening_factor(EE)
                    self._normL_ = 1. / quad(integrand,
                        self.pf['source_EminNorm'],
                        self.pf['source_EmaxNorm'], points=self.sharp_points)[0]

        return self._normL_

    #def _load_spectrum(self):
    #    """ Modify a few parameters if spectrum_file provided. """
    #
    #    fn = self.pf['spectrum_file']
    #
    #    if fn is None:
    #        return
    #
    #    # Read spectrum - expect hdf5 with (at least) E, LE, and t datasets.
    #    if re.search('.hdf5', fn):
    #        f = h5py.File(fn)
    #        try:
    #            self.pf['tables_times'] = f['t'].value
    #        except:
    #            self.pf['tables_times'] = None
    #            self.pf['spectrum_evolving'] = False
    #
    #        self.pf['spectrum_E'] = f['E'].value
    #        self.pf['spectrum_LE'] = f['LE'].value
    #        f.close()
    #
    #        if len(self.pf['spectrum_LE'].shape) > 1 \
    #            and not self.pf['spectrum_evolving']:
    #            self.pf['spectrum_LE'] = self.pf['spectrum_LE'][0]
    #    else:
    #        spec = readtab(fn)
    #        if len(spec) == 2:
    #            self.pf['spectrum_E'], self.pf['spectrum_LE'] = spec
    #        else:
    #            self.pf['spectrum_E'], self.pf['spectrum_LE'], \
    #                self.pf['spectrum_t'] = spec

    @property
    def tables(self):
        if not hasattr(self, '_tables'):
            self._create_integral_table()
        return self._tables

    @property
    def tab(self):
        if not hasattr(self, '_tab'):
            self._create_integral_table()
        return self._tab

    @property
    def tabs(self):
        if not hasattr(self, '_tabs'):
            self._create_integral_table()
        return self._tabs

    def _create_integral_table(self, logN=None):
        """
        Take tables and create interpolation functions.
        """

        if self.discrete:
            return

        if self._name == 'diffuse':
            return

        if self.pf['source_table'] is None:
            # Overide defaults if supplied - this is dangerous
            if logN is not None:
                self.pf.update({'tables_dlogN': [np.diff(tmp) for tmp in logN]})
                self.pf.update({'tables_logNmin': [np.min(tmp) for tmp in logN]})
                self.pf.update({'tables_logNmax': [np.max(tmp) for tmp in logN]})

            # Tabulate away!
            self._tab = IntegralTable(self.pf, self, self.grid, logN)
            self._tabs = self.tab.TabulateRateIntegrals()
        else:
            self._tab = IntegralTable(self.pf, self, self.grid, logN)
            self._tabs = self.tab.load(self.pf['source_table'])

        self._setup_interp()

    def _setup_interp(self):
        self._tables = {}
        for tab in self.tabs:
            self._tables[tab] = \
                LookupTable(self.pf, tab, self.tab.logN, self.tabs[tab],
                    self.tab.logx, self.tab.t)

    @property
    def sigma(self):
        """
        Compute bound-free absorption cross-section for all frequencies.
        """
        if not self.discrete:
            return None
        if not hasattr(self, '_sigma_all'):
            self._sigma_all = np.array(list(map(sigma_E, self.E)))

        return self._sigma_all

    def Qdot(self, t=None):
        """
        Returns number of photons emitted (s^-1) at all frequencies.
        """
        #if not hasattr(self, '_Qdot_all'):
        self._Qdot_all = self.Lbol(t) * self.LE / self.E / erg_per_ev

        return self._Qdot_all

    def hnu_bar(self, t=0):
        """
        Average ionizing (per absorber) photon energy in eV.
        """
        if not hasattr(self, '_hnu_bar_all'):
            self._hnu_bar_all = {}
        if not hasattr(self, '_qdot_bar_all'):
            self._qdot_bar_all = {}

        if t in self._hnu_bar_all:
            return self._hnu_bar_all[t]

        self._hnu_bar_all[t] = np.zeros_like(self.grid.zeros_absorbers)
        self._qdot_bar_all[t] = np.zeros_like(self.grid.zeros_absorbers)
        for i, absorber in enumerate(self.grid.absorbers):
            self._hnu_bar_all[t][i], self._qdot_bar_all[t][i] = \
                self._FrequencyAveragedBin(absorber=absorber, t=t)

        return self._hnu_bar_all

    def AveragePhotonEnergy(self, Emin, Emax):
        """
        Return average photon energy in supplied band.
        """

        integrand = lambda EE: self.Spectrum(EE) * EE
        norm = lambda EE: self.Spectrum(EE)

        return quad(integrand, Emin, Emax, points=self.sharp_points)[0] \
             / quad(norm, Emin, Emax, points=self.sharp_points)[0]

    @property
    def qdot_bar(self):
        """
        Average ionizing photon luminosity (per absorber) in s^-1.
        """
        if not hasattr(self, '_qdot_bar_all'):
            hnu_bar = self.hnu_bar

        return self._qdot_bar_all

    def erg_per_phot(self, Emin, Emax):
        return self.eV_per_phot(Emin, Emax) * erg_per_ev

    def eV_per_phot(self, Emin, Emax):
        """
        Compute the average energy per photon (in eV) in some band.
        """

        i1 = lambda E: self.Spectrum(E)
        i2 = lambda E: self.Spectrum(E) / E

        # Must convert units
        final = quad(i1, Emin, Emax, points=self.sharp_points)[0] \
              / quad(i2, Emin, Emax, points=self.sharp_points)[0]

        return final

    @property
    def sigma_bar(self):
        """
        Frequency averaged cross section (single bandpass).
        """
        if not hasattr(self, '_sigma_bar_all'):
            self._sigma_bar_all = np.zeros_like(self.grid.zeros_absorbers)
            for i, absorber in enumerate(self.grid.absorbers):
                integrand = lambda x: self.Spectrum(x) \
                    * self.grid.bf_cross_sections[absorber](x) / x

                self._sigma_bar_all[i] = self.Lbol \
                    * quad(integrand, self.grid.ioniz_thresholds[absorber],
                      self.Emax, points=self.sharp_points)[0] / self.qdot_bar[i] / erg_per_ev

        return self._sigma_bar_all

    @property
    def sigma_tilde(self):
        if not hasattr(self, '_sigma_tilde_all'):
            self._sigma_tilde_all = np.zeros_like(self.grid.zeros_absorbers)
            for i, absorber in enumerate(self.grid.absorbers):
                integrand = lambda x: self.Spectrum(x) \
                    * self.grid.bf_cross_sections[absorber](x)
                self._sigma_tilde_all[i] = quad(integrand,
                    self.grid.ioniz_thresholds[absorber], self.Emax,
                    points=self.sharp_points)[0] \
                    / self.fLbol_ionizing[i]

        return self._sigma_tilde_all

    @property
    def fLbol_ionizing(self, absorber=0):
        """
        Fraction of bolometric luminosity emitted above all ionization
        thresholds.
        """
        if not hasattr(self, '_fLbol_ioniz_all'):
            self._fLbol_ioniz_all = np.zeros_like(self.grid.zeros_absorbers)
            for i, absorber in enumerate(self.grid.absorbers):
                self._fLbol_ioniz_all[i] = quad(self.Spectrum,
                    self.grid.ioniz_thresholds[absorber], self.Emax,
                    points=self.sharp_points)[0]

        return self._fLbol_ioniz_all

    @property
    def Gamma_bar(self):
        """
        Return ionization rate (as a function of radius) assuming optical
        depth to cells and of cells is small.
        """
        if not hasattr(self, '_Gamma_bar_all'):
            self._Gamma_bar_all = \
                np.zeros([self.grid.dims, self.grid.N_absorbers])
            for i, absorber in enumerate(self.grid.absorbers):
                self._Gamma_bar_all[..., i] = self.Lbol * self.sigma_bar[i] \
                    * self.fLbol_ionizing[i] / 4. / np.pi / self.grid.r_mid**2 \
                    / self.hnu_bar[i] / erg_per_ev

        return self._Gamma_bar_all

    @property
    def gamma_bar(self):
        """
        Return ionization rate (as a function of radius) assuming optical
        depth to cells and of cells is small.
        """
        if not hasattr(self, '_gamma_bar_all'):
            self._gamma_bar_all = \
                np.zeros([self.grid.dims, self.grid.N_absorbers,
                    self.grid.N_absorbers])

            if not self.pf['secondary_ionization']:
                return self._gamma_bar_all

            for i, absorber in enumerate(self.grid.absorbers):
                for j, otherabsorber in enumerate(self.grid.absorbers):
                    self._gamma_bar_all[..., i, j] = self.Gamma_bar[j] \
                        * (self.hnu_bar[j] * self.sigma_tilde[j] \
                        /  self.hnu_bar[i] / self.sigma_bar[j] \
                        - self.grid.ioniz_thresholds[otherabsorber] \
                        / self.grid.ioniz_thresholds[absorber])

        return self._gamma_bar_all

    @property
    def Heat_bar(self):
        """
        Return ionization rate (as a function of radius) assuming optical
        depth to cells and of cells is small.
        """
        if not hasattr(self, '_Heat_bar_all'):
            self._Heat_bar_all = \
                np.zeros([self.grid.dims, self.grid.N_absorbers])
            for i, absorber in enumerate(self.grid.absorbers):
                self._Heat_bar_all[..., i] = self.Gamma_bar[..., i] \
                    * erg_per_ev * (self.hnu_bar[i] * self.sigma_tilde[i] \
                    / self.sigma_bar[i] - self.grid.ioniz_thresholds[absorber])

        return self._Heat_bar_all

    def IonizingPhotonLuminosity(self, t=0, bin=None):
        """
        Return Qdot (photons / s) for this source at energy E.
        """

        if self.pf['source_type'] in [0, 1, 2]:
            return self.Qdot[bin]
        else:
            # Currently only BHs have a time-varying bolometric luminosity
            return self.BolometricLuminosity(t) * self.LE[bin] / self.E[bin] / erg_per_ev

    #def _Intensity(self, E, i, Type, t=0, absorb=True):
    #    """
    #    Return quantity *proportional* to fraction of bolometric luminosity emitted
    #    at photon energy E.  Normalization handled separately.
    #    """
    #
    #    Lnu = self.src._Intensity(E, i, Type, t=t)
    #
    #    # Apply absorbing column
    #    if self.SpectrumPars['logN'][i] > 0 and absorb:
    #        return Lnu * np.exp(-10.**self.SpectrumPars['logN'][i] \
    #            * (sigma_E(E, 0) + y * sigma_E(E, 1)))
    #    else:
    #        return Lnu
    #
    def Spectrum(self, E, t=0.0):
        r"""
        Return fraction of bolometric luminosity emitted at energy E.

        Elsewhere denoted as :math:`I_{\nu}`, normalized such that
        :math:`\int I_{\nu} d\nu = 1`

        Parameters
        ----------
        E: float
            Emission energy in eV
        t: float
            Time in seconds since source turned on.
        i: int
            Index of component to include. If None, includes contribution
            from all components.

        Returns
        -------
        Fraction of bolometric luminosity emitted at E in units of
        eV\ :sup:`-1`\.

        """

        if self.pf['source_Ekill'] is not None:
            if self.pf['source_Ekill'][0] <= E <= self.pf['source_Ekill'][1]:
                return 0.0

        return self._normL * self._Intensity(E, t=t)

    def BolometricLuminosity(self, t=0.0, M=None):
        """
        Returns the bolometric luminosity of a source in units of erg/s.
        For accreting black holes, the bolometric luminosity will increase
        with time, hence the optional 't' and 'M' arguments.
        """

        if self._name == 'bh':
            return self.Luminosity(t, M)
        else:
            return self.Luminosity(t)

    def _FrequencyAveragedBin(self, absorber='h_1', Emin=None, Emax=None,
        energy_weighted=False, t=0):
        """
        Bolometric luminosity / number of ionizing photons in spectrum in bandpass
        spanning interval (Emin, Emax). Returns mean photon energy and number of
        ionizing photons in band.
        """

        if Emin is None:
            Emin = max(self.grid.ioniz_thresholds[absorber], self.Emin)
        if Emax is None:
            Emax = self.Emax

        if energy_weighted:
            f = lambda x: x
        else:
            f = lambda x: 1.0

        L = self.Lbol * quad(lambda x: self.Spectrum(x) * f(x), Emin, Emax,
            points=self.sharp_points)[0]
        Q = self.Lbol * quad(lambda x: self.Spectrum(x) * f(x) / x, Emin,
            Emax, points=self.sharp_points)[0] / erg_per_ev

        return L / Q / erg_per_ev, Q

    def dump(self, fn, E, clobber=False):
        """
        Write SED out to file.

        Parameters
        ----------
        fn : str
            Filename, suffix determines type. If 'hdf5' or 'h5' will write
            to HDF5 file, otherwise, to ASCII.
        E : np.ndarray
            Array of photon energies at which to sample SED. Units = eV.

        """

        if os.path.exists(fn) and (clobber == False):
            raise OSError('{!s} exists!'.format(fn))

        if re.search('.hdf5', fn) or re.search('.h5', fn):
            out = 'hdf5'
        else:
            out = 'ascii'

        LE = list(map(self.Spectrum, E))

        if out == 'hdf5':
            f = h5py.File(fn, 'w')
            f.create_dataset('E', data=E)
            f.create_dataset('LE', data=LE)
            f.close()
        else:
            f = open(fn, 'w')
            print("# E     LE", file=f)
            for i, nrg in enumerate(E):
                print("{0:.8e} {1:.8e}".format(nrg, LE[i]), file=f)
            f.close()

        print("Wrote {!s}.".format(fn))

    def sed_name(self, i=0):
        """
        Return name of output file based on SED properties.
        """

        name = ('{0!s}_logM_{1:.2g}_Gamma_{2:.3g}_fsc_{3:.3g}_' +\
            'logE_{4:.2g}-{5:.2g}').format(self.SpectrumPars['type'][i],\
            np.log10(self.src.M0), self.src.spec_pars['alpha'][i],
            self.src.spec_pars['fsc'][i], np.log10(self.Emin), np.log10(self.Emax))

        return name


# /usr/bin/python3
# coding=utf-8
# Point Of No Return (Training)

"""
Copy of the game file, includes only nessessary content and no graphical code.

@author: anon-42
@version: beta
"""

import numpy as np
import ann#artifical_neural_net as ann
import act_func as af
import trainer as tr
import replay_memory as rm
import os
import sys
import history as hs
import time
from threading import Thread

sys.setrecursionlimit(10000)

class Dot:

    """
    Class for the points the player can visit.
    """

    def __init__(self, x, y, state=0):
        self.x = x
        self.y = y
        self.setstate(state)

    def setstate(self, state): # -1 = bereits besucht, 0 = unbesetzt, 1 = aktuell besetzt
        """
        Sets the state of Dot.
        """
        self.state = state

class PONR:

    """
    Main class of the game.
    """

    def __init__(self, P1, P2, len_x=17, len_y=9, goal=5):
        self.pos = [int(len_x / 2), int(len_y / 2)] # Position des Spielers
        self.lines_data = [np.array([0.0 for i in range((len_x - 1) * len_y)]),            # waagerecht
                           np.array([0.0 for i in range(len_x * (len_y - 1))]),               # senkrecht
                           np.array([0.0 for i in range((len_x - 1) * (len_y - 1))]),# diagonal lo ru
                           np.array([0.0 for i in range((len_x - 1) * (len_y - 1))])]# diagonal ro lu
        self.touched_points = [self.pos]
        self.diagonals = []
        self.P1 = P1
        self.P2 = P2
        if len_x % 2 == len_y % 2 == goal % 2 == 1:
            self.size = [len_x, len_y]
            self.goal = goal
        else:
            raise ValueError('len_x, len_y and goal must be odd numbers.')

        self.Dots = []
        for x in range(self.size[0]):
            for y in range(self.size[1]):
                self.Dots.append(Dot(x, y))
        self.find_Dot(self.pos).setstate(1)

    def start(self):
        """
        Mainloop of the game.
        """
        active, passive = self.P1, self.P2
        try:
            while True:
                for turn_number in range(3):
                    if self.player_turn(active, turn_number):
                        continue
                    else:
                        for turn_number in range(6):
                            self.player_turn(passive, turn_number, free_kick=True)
                        active, passive = passive, active
                        break
                active, passive = passive, active
        except IndexError:
            return

    def find_Dot(self, pos):
        """
        Finds a Dot object in self.Dots given its coordinates and returns it.
        """
        for Dot in self.Dots:
            if Dot.x == pos[0] and Dot.y == pos[1]:
                return Dot

    def player_turn(self, player, turn_number, free_kick=False, repetition=False):
        """
        Executes one player turn.
        """
        prev_pos = self.pos
        if not free_kick and not self.can_move():
            return False
        _foo = [0.0 for i in range(6)]
        _foo[turn_number] = 1.0
        _foo.append(int(free_kick))
        if player == self.P2:
            state = np.append(np.array(_foo), np.fliplr(np.array([np.concatenate(self.lines_data)])))
        else:
            state = np.append(np.array(_foo), np.concatenate(self.lines_data))
        step = player.get_input(state)
        new_pos = [self.pos[0] + step[0],
                   self.pos[1] + step[1]]
        new_state = np.array([])
        if turn_number == 5 and new_pos in self.touched_points:
            if player == self.P1:
                reward = -1
            else:
                reward = 1
            self.update(player, state, step, reward, new_state)
            raise IndexError
        elif new_pos[1] in [y for y in range((self.size[1] - self.goal) // 2,    # goal left
                                             (self.size[1] + self.goal) // 2)] and new_pos[0] == -1:
            reward = -1
            self.update(player, state, step, reward, new_state)
            raise IndexError
        elif new_pos[1] in [y for y in range((self.size[1] - self.goal) // 2,    # goal right
                                             (self.size[1] + self.goal) // 2)] and new_pos[0] == self.size[0]:
            reward = 1
            self.update(player, state, step, reward, new_state)
            raise IndexError
        elif self.rules(prev_pos, new_pos, free_kick):
            index = (+ min(prev_pos[1], new_pos[1])
                     * (self.size[0] - 1)
                     + min(prev_pos[0], new_pos[0]))
            if step[0] == 0:        # senkrecht
                self.lines_data[1][index] = 1
            elif step[1] == 0:      # waagerecht
                self.lines_data[0][index] = 1
            elif step[0] == step[1]:# diagonal ro lu
                self.lines_data[3][index] = 1
            else:                   # diagonal lo ru
                self.lines_data[2][index] = 1
            self.pos = new_pos
            self.find_Dot(prev_pos).setstate(-1)
            self.find_Dot(new_pos).setstate(1)
            self.touched_points.append(new_pos)
            self.diagonals.append([prev_pos, new_pos])
            reward = self.reward(repetition, new_pos, prev_pos)
            _foo = [0.0 for i in range(6)]
            _foo[turn_number] = 1.0
            _foo.append(int(free_kick))
            if player == self.P2:
                new_state = np.append(np.array(_foo),
                                      np.fliplr(np.array([np.concatenate(self.lines_data)])))
            else:
                new_state = np.append(np.array(_foo),
                                      np.concatenate(self.lines_data))
        else:
            print(time.clock())
            self.player_turn(player, turn_number, free_kick, repetition=True)
            reward = self.reward(repetition, new_pos, prev_pos)
        if player == self.P2:
            step = [-step[0], -step[1]]
        self.update(player, state, step, reward, new_state)
        return True

    def can_move(self):
        """
        Checks if the player can move.
        """
        for step in [[-1, -1], [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1],[-1, 0]]:
            if self.rules(self.pos, [self.pos[0] + step[0], self.pos[1] + step[1]], False):
                return True
        if self.pos[0] in [0, self.size[1]] and self.pos[1] in [y for y in range((self.size[1] - self.goal) // 2, (self.size[1] + self.goal) // 2)]:
            return True
        return False

    def rules(self, prev_pos, new_pos, free_kick):
        """
        Checks if all rules were followed.
        """
        if (prev_pos[0] != new_pos[0]) and (prev_pos[1] != new_pos[1]):                     #"Wenn es eine Diagonale ist, dann..."
            cd = [[new_pos[0], prev_pos[1]], [prev_pos[0], new_pos[1]]]                     #cd = corresponding diagonal
            if not (cd in self.diagonals or [cd[1], cd[0]] in self.diagonals):              #"Wenn cd nicht in der Liste der bereits vorhandenen Diagonalen, dann..."
                a = True                                                                    #Regeln eingehalten; Diagonale kann gezeichnet werden
            else:
                a = False
        else:
            a = True                                                                        #Regeln eingehalten, aber es ist keine Diagonale
                                                                                #Regeln nicht eingehalten; Diagonale darf nicht gezeichnet werden
        return ((0 <= new_pos[0] < self.size[0] and 0 <= new_pos[1] < self.size[1]) and     #Spielfeldgroesse
                ((not new_pos in self.touched_points) or free_kick) and                                    #Betretene Punkte nicht erneut betreten
                (a or free_kick))                                                                        #Kreuzen der bereits vorhandenen Diagonalen

    def reward(self, penalty, new_pos, prev_pos):
        """
        Calculates a reward for a turn.
        """
        if penalty:
            return -0.5
        elif new_pos[0] > prev_pos[0]:
            return -0.01
        elif new_pos[0] < prev_pos[0]:
            return -0.02
        elif new_pos[0] == prev_pos[0]:
            return -0.015

    def update(self, player, state, action, reward, new_state):
        """
        TODO: add docstring
        """
        global history, rm
        rm.update(state, action, reward, new_state)
        history.add_turn([(str(int(player == self.P1)), state, np.array(action), reward)])

    def game_replay(self):
        pass

class Interface:

    """
    The game interface to a player (human / AI).
    """
    def __init__(self, name=None):
        global eta, alpha, net, history
        self.net = net
        self.trainer = tr.Trainer(self.net, eta, alpha, history)
        self.name = name if name != None else '<empty>'
        self.iterations = 0

    def get_input(self, data):
        """
        Gets an input from the ann.
        """
        global counter
        Qvalues = self.net.forward(np.array([data])) # 8 element array

        # probability to choose an action ==> Boltzmann exploration
        Qvalues /= (50000/counter + 0.5) # T - temperature
        counter += 1

        # softmax
        e_x = np.exp(Qvalues - np.max(Qvalues))
        prob = e_x / e_x.sum()
        self.step = [[-1, -1],
                     [0, -1],
                     [1, -1],
                     [1, 0],
                     [1, 1],
                     [0, 1],
                     [-1, 1],
                     [-1, 0]][np.random.choice(np.arange(8), p=prob.flatten())]

        self.iterations += 1
        return self.step

    def train_net(self):
        """
        Passes the current game stats to the AI.
        """
        global number_of_turns, data_from_rm, rm
        # get trainig data from replay_memory
        rm.number_of_turns(data_from_rm)
        data = rm.get()

        state = np.array([data[0]])
        action = np.array([data[1]])
        reward = np.array([[data[2]]])
        new_state = np.array([data[3]])

        for element in rm:
            state = np.append(state, (np.array([element[0]])), axis=0)
            action = np.append(action, (np.array([element[1]])), axis=0)
            reward = np.append(reward, (np.array([[element[2]]])), axis=0)
            new_state = np.append(new_state, (np.array([element[3]])), axis=0)

        # compute desired output for training purposes
        correctoutput = self.net.forward(state)
        maxQ = np.amax(self.net.forward(new_state), axis=1, keepdims=True)

        # final state
        maxQ[np.where(np.logical_or(reward == 1, reward == -1)), :] = 0

        index_gamma09 = np.logical_or(np.logical_and(state[:, 6] == 1,
          state[:, 5] == 1), np.logical_and(state[:,6]==0, state[:, 2] == 1))
        gamma = np.where(index_gamma09, 0.9, 1)
        gamma = np.reshape(gamma, (-1, 1))

        action_taken = ((action[:,1]+2)**2 + action[:,0]).flatten()
        correctoutput[np.where(action_taken > 3, action_taken-1,
          action_taken)] = reward + gamma*maxQ

        self.trainer.train(state, correctoutput, number_of_turns)

class Saver(Thread):

    """
    Thread for saving ANN and ReplayMemory instances once in 900 seconds.
    """

    def __init__(self, net, rm):
        Thread.__init__(self)
        self.net = net
        self.rm = rm
        self.stop = False

    def run(self):
        """
        The threads main activity.
        """
        while not self.stop:
            self.net.save()
            self.rm.save()
            time.sleep(900)

    def stop(self):
        """
        Prevents the thread from continuing when called.
        """
        self.stop = True

if __name__ == '__main__':
    global eta, alpha, number_of_turns, data_from_rm, rm, history, net, counter
    path = os.path.dirname(os.path.abspath(__file__)) + '/saves/'    # must end with "/" on Linux and with "\" on Windows
    history = hs.History(path + 'History')
    rm = rm.ReplayMemory(path + 'ReplayMemory.rm', 42000)
    Lambda = .0
    eta = .0001
    alpha = .7
    input_layer = 543
    output_layer = (8, af.tanh)
    hidden_layers = [(543, af.tanh),
                     (543, af.tanh),
                     (543, af.tanh)]
    number_of_turns = 500
    data_from_rm = 500
    net = ann.Neural_Network(path + 'DATA',
                             input_layer,
                             output_layer,
                             hidden_layers,
                             Lambda)
    Saver(net, rm).start()
    counter = 1
    while True:
        history.setGame(hs.generateName('main_net', 'dummy_net', 1).__next__())
        GAME = PONR(Interface('main net'),
                    Interface('dummy net'))
        GAME.start()


# Copyright (C) 2013 Atsushi Togo
# All rights reserved.
#
# This file is part of phonopy.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in
#   the documentation and/or other materials provided with the
#   distribution.
#
# * Neither the name of the phonopy project nor the names of its
#   contributors may be used to endorse or promote products derived
#   from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import numpy as np

class DerivativeOfDynamicalMatrix(object):
    """Compute analytical derivative of dynamical matrix

    This can be used dynamical matrix without NAC or with Wang-NAC.

    """

    def __init__(self, dynamical_matrix):
        self._dynmat = dynamical_matrix
        (self._smallest_vectors,
         self._multiplicity) = self._dynmat.get_shortest_vectors()
        self._force_constants = self._dynmat.get_force_constants()
        self._scell = self._dynmat.get_supercell()
        self._pcell = self._dynmat.get_primitive()

        self._p2s_map = self._dynmat.get_primitive_to_supercell_map()
        self._s2p_map = self._dynmat.get_supercell_to_primitive_map()
        p2p_map = self._pcell.get_primitive_to_primitive_map()
        self._s2pp_map = np.array(
            [p2p_map[self._s2p_map[i]] for i in range(len(self._s2p_map))],
            dtype='intc')
        self._mass = self._pcell.get_masses()

        self._ddm = None

        # Derivative order=2 can work only within the following conditions:
        # 1. Second derivative of NAC is not considered.
        # 2. Python implementation
        self._derivative_order = None

    def run(self, q, q_direction=None, lang='C'):
        if self._derivative_order is not None or lang != 'C':
            self._run_py(q, q_direction=q_direction)
        else:
            self._run_c(q, q_direction=q_direction)

    def set_derivative_order(self, order):
        if order == 1 or order == 2:
            self._derivative_order = order
        else:
            print("Error: derivative order has to be 1 or 2")

    def get_derivative_of_dynamical_matrix(self):
        return self._ddm

    def _run_c(self, q, q_direction=None):
        import phonopy._phonopy as phonoc
        num_patom = len(self._p2s_map)

        mass = self._pcell.get_masses()
        fc = self._force_constants
        itemsize = self._force_constants.itemsize
        ddm = np.zeros((3, num_patom * 3, num_patom * 3),
                       dtype=("c%d" % (itemsize * 2)))
        vectors = self._smallest_vectors
        multiplicity = self._multiplicity
        if self._dynmat.is_nac():
            born = self._dynmat.get_born_effective_charges()
            dielectric = self._dynmat.get_dielectric_constant()
            nac_factor = self._dynmat.get_nac_factor()
            if q_direction is None:
                q_dir = None
            else:
                q_dir = np.array(q_direction, dtype='double', order='C')
        else:
            born = None
            dielectric = None
            nac_factor = 0
            q_dir = None

        if fc.shape[0] == fc.shape[1]: # full fc
            phonoc.derivative_dynmat(ddm.view(dtype='double'),
                                     fc,
                                     np.array(q, dtype='double'),
                                     np.array(self._pcell.get_cell().T,
                                              dtype='double', order='C'),
                                     vectors,
                                     multiplicity,
                                     mass,
                                     self._s2p_map,
                                     self._p2s_map,
                                     nac_factor,
                                     born,
                                     dielectric,
                                     q_dir)
        else:
            phonoc.derivative_dynmat(ddm.view(dtype='double'),
                                     fc,
                                     np.array(q, dtype='double'),
                                     np.array(self._pcell.get_cell().T,
                                              dtype='double', order='C'),
                                     vectors,
                                     multiplicity,
                                     mass,
                                     self._s2pp_map,
                                     np.arange(len(self._p2s_map),
                                               dtype='intc'),
                                     nac_factor,
                                     born,
                                     dielectric,
                                     q_dir)

        self._ddm = ddm

    def _run_py(self, q, q_direction=None):
        if self._dynmat.is_nac():
            if q_direction is None:
                fc_nac = self._nac(q)
                d_nac = self._d_nac(q)
            else:
                fc_nac = self._nac(q_direction)
                d_nac = self._d_nac(q_direction)

        fc = self._force_constants
        vecs = self._smallest_vectors
        multiplicity = self._multiplicity
        num_patom = len(self._p2s_map)
        num_satom = len(self._s2p_map)

        if self._derivative_order == 2:
            num_elem = 6
        else:
            num_elem = 3

        itemsize = self._force_constants.itemsize
        ddm = np.zeros((num_elem, 3 * num_patom, 3 * num_patom),
                       dtype=("c%d" % (itemsize * 2)))

        for i, j in list(np.ndindex(num_patom, num_patom)):
            s_i = self._p2s_map[i]
            s_j = self._p2s_map[j]
            mass = np.sqrt(self._mass[i] * self._mass[j])
            ddm_local = np.zeros((num_elem, 3, 3),
                                 dtype=("c%d" % (itemsize * 2)))

            for k in range(num_satom):
                if s_j != self._s2p_map[k]:
                    continue

                multi = multiplicity[k, i]
                vecs_multi = vecs[k, i, :multi]
                phase_multi = np.exp([np.vdot(vec, q) * 2j * np.pi
                                      for vec in vecs_multi])
                vecs_multi_cart = np.dot(vecs_multi, self._pcell.get_cell())
                coef_order1 = 2j * np.pi * vecs_multi_cart
                if self._derivative_order == 2:
                    coef_order2 = [np.outer(co1, co1) for co1 in coef_order1]
                    coef = np.array([co2.ravel()[[0, 4, 8, 5, 2, 1]]
                                     for co2 in coef_order2])
                else:
                    coef = coef_order1

                if self._dynmat.is_nac():
                    fc_elem = fc[s_i, k] + fc_nac[i, j]
                else:
                    fc_elem = fc[s_i, k]

                for l in range(num_elem):
                    ddm_elem = fc_elem * (coef[:, l] * phase_multi).sum()
                    if (self._dynmat.is_nac() and
                        not self._derivative_order == 2):
                        ddm_elem += d_nac[l, i, j] * phase_multi.sum()

                    ddm_local[l] +=  ddm_elem / mass / multi

            ddm[:, (i * 3):(i * 3 + 3), (j * 3):(j * 3 + 3)] = ddm_local

        # Impose Hermite condition
        self._ddm = np.array([(ddm[i] + ddm[i].conj().T) / 2
                              for i in range(num_elem)])

    def _nac(self, q_direction):
        """nac_term = (A1 (x) A2) / B * coef.
        """
        num_atom = self._pcell.get_number_of_atoms()
        nac_q = np.zeros((num_atom, num_atom, 3, 3), dtype='double')
        if (np.abs(q_direction) < 1e-5).all():
            return nac_q

        rec_lat = np.linalg.inv(self._pcell.get_cell())
        nac_factor = self._dynmat.get_nac_factor()
        Z = self._dynmat.get_born_effective_charges()
        e = self._dynmat.get_dielectric_constant()
        q = np.dot(rec_lat, q_direction)

        B = self._B(e, q)
        for i in range(num_atom):
            A_i = self._A(q, Z, i)
            for j in range(num_atom):
                A_j = self._A(q, Z, j)
                nac_q[i, j] = np.outer(A_i, A_j) / B

        num_satom = self._scell.get_number_of_atoms()
        N = num_satom // num_atom

        return nac_q * nac_factor / N

    def _d_nac(self, q_direction):
        num_atom = self._pcell.get_number_of_atoms()
        d_nac_q = np.zeros((3, num_atom, num_atom, 3, 3), dtype='double')
        if (np.abs(q_direction) < 1e-5).all():
            return d_nac_q

        rec_lat = np.linalg.inv(self._pcell.get_cell())
        nac_factor = self._dynmat.get_nac_factor()
        Z = self._dynmat.get_born_effective_charges()
        e = self._dynmat.get_dielectric_constant()
        q = np.dot(rec_lat, q_direction)

        B = self._B(e, q)
        for xyz in range(3):
            dB = self._dB(e, q, xyz)
            for i in range(num_atom):
                A_i = self._A(q, Z, i)
                dA_i = self._dA(Z, i, xyz)
                for j in range(num_atom):
                    A_j = self._A(q, Z, j)
                    dA_j = self._dA(Z, j, xyz)
                    d_nac_q[xyz, i, j] = (
                        (np.outer(dA_i, A_j) + np.outer(A_i, dA_j)) / B -
                        np.outer(A_i, A_j) * dB / B ** 2)

        num_satom = self._scell.get_number_of_atoms()
        N = num_satom // num_atom
        return d_nac_q * nac_factor / N

    def _A(self, q, Z, atom_num):
        return np.dot(q, Z[atom_num])

    def _B(self, epsilon, q):
        return np.dot(q, np.dot(epsilon, q))

    def _dA(self, Z, atom_num, xyz):
        return Z[atom_num, xyz, :]

    def _dB(self, epsilon, q, xyz):
        e = epsilon
        return np.dot(e[xyz], q) * 2


# -*- coding: utf-8 -*-
"""
ITU-R BT.2020 Colourspace
=========================

Defines the *ITU-R BT.2020* colourspace:

-   :attr:`colour.models.BT2020_COLOURSPACE`.

References
----------
-   :cite:`InternationalTelecommunicationUnion2015h` : International
    Telecommunication Union. (2015). Recommendation ITU-R BT.2020 - Parameter
    values for ultra-high definition television systems for production and
    international programme exchange. Retrieved from https://www.itu.int/\
dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-2-201510-I!!PDF-E.pdf
"""

from __future__ import division, unicode_literals

import numpy as np

from colour.colorimetry import ILLUMINANTS
from colour.models.rgb import (RGB_Colourspace, normalised_primary_matrix,
                               oetf_BT2020, eotf_BT2020)

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2020 - Colour Developers'
__license__ = 'New BSD License - https://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-developers@colour-science.org'
__status__ = 'Production'

__all__ = [
    'BT2020_PRIMARIES', 'BT2020_WHITEPOINT_NAME', 'BT2020_WHITEPOINT',
    'BT2020_TO_XYZ_MATRIX', 'XYZ_TO_BT2020_MATRIX', 'BT2020_COLOURSPACE'
]

BT2020_PRIMARIES = np.array([
    [0.7080, 0.2920],
    [0.1700, 0.7970],
    [0.1310, 0.0460],
])
"""
*ITU-R BT.2020* colourspace primaries.

BT2020_PRIMARIES : ndarray, (3, 2)
"""

BT2020_WHITEPOINT_NAME = 'D65'
"""
*ITU-R BT.2020* colourspace whitepoint name.

BT2020_WHITEPOINT_NAME : unicode
"""

BT2020_WHITEPOINT = (
    ILLUMINANTS['CIE 1931 2 Degree Standard Observer'][BT2020_WHITEPOINT_NAME])
"""
*ITU-R BT.2020* colourspace whitepoint.

BT2020_WHITEPOINT : ndarray
"""

BT2020_TO_XYZ_MATRIX = normalised_primary_matrix(BT2020_PRIMARIES,
                                                 BT2020_WHITEPOINT)
"""
*ITU-R BT.2020* colourspace to *CIE XYZ* tristimulus values matrix.

BT2020_TO_XYZ_MATRIX : array_like, (3, 3)
"""

XYZ_TO_BT2020_MATRIX = np.linalg.inv(BT2020_TO_XYZ_MATRIX)
"""
*CIE XYZ* tristimulus values to *ITU-R BT.2020* colourspace matrix.

XYZ_TO_BT2020_MATRIX : array_like, (3, 3)
"""

BT2020_COLOURSPACE = RGB_Colourspace(
    'ITU-R BT.2020',
    BT2020_PRIMARIES,
    BT2020_WHITEPOINT,
    BT2020_WHITEPOINT_NAME,
    BT2020_TO_XYZ_MATRIX,
    XYZ_TO_BT2020_MATRIX,
    oetf_BT2020,
    eotf_BT2020,
)
BT2020_COLOURSPACE.__doc__ = """
*ITU-R BT.2020* colourspace.

References
----------
:cite:`InternationalTelecommunicationUnion2015h`

BT2020_COLOURSPACE : RGB_Colourspace
"""