import csv
import os
import itertools
import ast
import pandas as pd
from sklearn.metrics.pairwise import pairwise_distances
import pickle
from sklearn import metrics
import numpy as np

# similarity based (noaudio)
# popular rec (pop)
# greatest artist (sagh)
# collocation artist (cagh) - similarity based (noaudio - artist-level)

# What's missing in here.
# similarity based (audio) - should be matched with mixset sequencing
# audio based EDM artist classifier (feature) similarity based vs Rachel's shortest path

# read sequencing.csv
playlists = {}
with open('sequencing.csv', mode='r') as infile:
    reader = csv.DictReader(infile)
    for row in reader:
        data = dict(row)
        playlist_id = data['id']
        tracks_in_playlist = ast.literal_eval(data['track_id'])
        playlists[playlist_id] = tracks_in_playlist

print(len(playlists))

tracks = playlists.values()
seed_list = [x[0] for x in tracks]
tracks = list(itertools.chain(*tracks))
tracks = list(set(tracks))
print(len(playlists))
print(len(tracks))
print(seed_list)
print(len(seed_list))

# total songs / binary matrix
df = pd.concat([pd.Series(v, name=k).astype(str) for k, v in playlists.items()], axis=1)
df = pd.get_dummies(df.stack()).sum(level=1).clip_upper(1)
print(df.shape)

# replace playlist index to the first item id of playlists
org_list = df.index.tolist()
df = df.rename(index=dict(zip(org_list,seed_list)))

print(df.head())
# for cagh, artist level playlist sim mat
# co-occur hammming distance similarity matrix (songXsong)
jac_sim = 1 - pairwise_distances(df.T, metric = "hamming")
# optionally convert it to a DataFrame
jac_sim = pd.DataFrame(jac_sim, index=df.columns, columns=df.columns)
print(jac_sim.shape)
print(jac_sim.head)
store = pd.HDFStore('jac_sim.h5')
store['jac_sim'] = jac_sim
store.close()

# greatest artist pop_seed
# read track_sub, find matching artist - artist-track dictionary - set used artist's tracks to 1 similarity score followed by popularity ranking
artist_to_track = {}
track_to_artist = {}
with open('track_subsub.csv', mode='r') as infile:
    reader = csv.DictReader(infile)
    for row in reader:
        data = dict(row)
        track_id = data['track_id']
        artist_id = data['artist_id']
        artist_to_track.setdefault(artist_id, []).append(track_id)
        track_to_artist[track_id] = artist_id

# sim mat based ranking for each seed (up to 300, 50 interval?) / df: ground truth
# AUC and mAP
jac_sim = pd.read_hdf('jac_sim.h5')

# set diagonal term to 0 to avoid self recommendation
jac_sim.values[[np.arange(jac_sim.shape[0])]*2] = 0

# set groundtruth self to 0
for i,row in df.iterrows():
    df.set_value(i, i, 0)
print(df.head())

# popularity based
pops = df.sum(axis=0) # np.max(pop_seed) = 32
pop_seed = pops.values

auc_noaudio = []
auc_pop = []
auc_sagh = []
for iter in range(len(seed_list)):
    # ground truth
    gt_seed = df.iloc[iter,:].values

    # jaccard similarity based
    noaudio_seed = jac_sim.loc[seed_list[iter],:].values

    # greatest artist pop seed
    seed_artist_tracks = artist_to_track[track_to_artist[seed_list[iter]]]
    sagh = pops.copy()
    for iter2 in range(len(seed_artist_tracks)):
        if seed_artist_tracks[iter2] in sagh.index:
            sagh.loc[seed_artist_tracks[iter2]] = 100 * sagh.loc[seed_artist_tracks[iter2]] # 100 * previous values
        else:
            continue
    sagh_seed = sagh.values

    auc_noaudio_seed = metrics.roc_auc_score(gt_seed,noaudio_seed)
    auc_noaudio.append(auc_noaudio_seed)
    print('auc_noaudio: ', np.mean(np.asarray(auc_noaudio)))

    auc_pop_seed = metrics.roc_auc_score(gt_seed,pop_seed)
    auc_pop.append(auc_pop_seed)
    print('auc_pop: ', np.mean(np.asarray(auc_pop)))

    auc_sagh_seed = metrics.roc_auc_score(gt_seed,sagh_seed)
    auc_sagh.append(auc_sagh_seed)
    print('auc_sagh: ', np.mean(np.asarray(auc_sagh)))


import collections
import itertools
import logging
import random
import string
import threading
import time
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Mapping,
    Optional,
    Set,
    Tuple,
    Union,
)

import doorpi.actions
import doorpi.event

from . import log

LOGGER: doorpi.DoorPiLogger = logging.getLogger(__name__)  # type: ignore

ActionCallable = Callable[[str, Mapping[str, Any]], Any]
RegistrableAction = Union[str, "doorpi.actions.Action", ActionCallable]

def generate_id() -> str:
    """Generates a random event ID."""
    size = 6
    chars = "".join((string.ascii_uppercase, string.digits))
    return "".join(random.choice(chars) for _ in range(size))

class EventHandler:
    """The event handler and action dispatcher."""

    actions: Dict[str, List[ActionCallable]]
    events: Dict[str, Set[str]]
    extra_info: Dict[str, Any]
    sources: List[str]

    __active: bool

    def __init__(self) -> None:
        conf = doorpi.INSTANCE.config
        db_path = conf["eventlog"]
        self.log = log.EventLog(db_path)

        self.actions = collections.defaultdict(list)
        self.events = collections.defaultdict(set)
        self.extra_info = {"LastKey": "NotSetYet", "event": {}}
        self.sources = []
        self.__active = True

        # register eventbased actions from configfile
        for event, actions in conf.view("events").items():
            LOGGER.info("Registering configured actions for %s", event)
            for action in actions:
                LOGGER.debug("Registering action %s", repr(action))
                self.register_action(event, action)

        # register configured DTMF actions
        LOGGER.info("Registering DTMF actions")
        for seq, actions in conf.view("sipphone.dtmf").items():
            for action in actions:
                LOGGER.debug("Registering action %r for DTMF %r", action, seq)
                self.register_action(f"OnDTMF_{seq}", action)

    def destroy(self) -> None:
        """Shut down the event handler"""
        self.__active = False
        self.log.destroy()

    @property
    def event_history(self) -> Tuple[log.EventLogEntry, ...]:
        return self.log.get_event_log()

    @property
    def threads(self) -> List[threading.Thread]:
        """List event threads managed by the handler"""
        return [
            t
            for t in threading.enumerate()
            if t.name.startswith("DoorPi Event")
        ]

    @property
    def idle(self) -> bool:
        """Return whether the handler is currently idle"""
        return not self.threads

    def get_events_by_source(self, source: str) -> Set[str]:
        """Group all known events by the sources that can fire them"""
        return {ev for ev in self.events if source in self.events[ev]}

    def register_source(self, source: str) -> None:
        """Register a new event source"""
        if source not in self.sources:
            self.sources.append(source)
            LOGGER.debug("Added event source %s", source)

    def register_event(self, event: str, source: str) -> None:
        """Register an event to be fired from the named event source"""
        suppress_logs = _suppress_logs(event)
        if not suppress_logs:
            LOGGER.debug("Registering event %s with source %s", event, source)
        self.register_source(source)

        if source not in self.events[event]:
            self.events[event].add(source)
            if not suppress_logs:
                LOGGER.debug(
                    "Registered source %s for event %s", source, event
                )
        else:
            LOGGER.warning(
                "Multiple registrations for event %s from source %s",
                event,
                source,
            )

    def fire_event(
        self, event: str, source: str, *, extra: Dict[str, Any] = None
    ) -> None:
        """Fire an event asynchronously"""
        if not self.__active:
            return
        threading.Thread(
            target=self.fire_event_sync,
            args=(event, source),
            kwargs={"extra": extra},
            name=f"DoorPi Event {event} from {source}",
        ).start()

    def fire_event_sync(
        self, event: str, source: str, *, extra: Dict[str, Any] = None
    ) -> None:
        """Fire an event synchronously"""
        if not self.__active:
            return

        if extra is None:
            extra = {}
        suppress_logs = _suppress_logs(event)

        if source not in self.sources:
            LOGGER.warning(
                "Unknown event source %s, skipping %s", source, event
            )
            return
        if event not in self.events:
            LOGGER.warning(
                "Unknown event %s (from source %s), skipping", event, source
            )
            return
        if source not in self.events[event]:
            LOGGER.warning(
                "Source %s not registered for %s, skipping", source, event
            )
            return

        if event not in self.actions:
            if not suppress_logs:
                LOGGER.debug("No actions registered for %s, skipping", event)
            return

        event_id = generate_id()
        start_time = time.time()

        if not suppress_logs:
            self.log.log_event(event_id, source, event, start_time, extra)

        extra.update(
            {
                "last_fired": str(start_time),
                "source": source,
                "event_id": event_id,
            }
        )

        # copy over info from last event run
        last_info = self.extra_info.get(event, {})
        for key in ["last_finished", "last_duration"]:
            extra[key] = last_info.get(key, None)
        self.extra_info[event] = extra

        if not suppress_logs:
            LOGGER.debug(
                "[%s] Executing %d action(s) for %s",
                event_id,
                len(self.actions[event]),
                event,
            )

        oneshot_actions = []
        for action in self.actions[event]:
            try:
                if not suppress_logs:
                    LOGGER.debug("[%s] Executing %s", event_id, action)
                action(event_id, extra)
                if not suppress_logs:
                    self.log.log_action(event_id, str(action), start_time)
            except doorpi.event.AbortEventExecution:
                LOGGER.info("[%s] Aborting event execution early")
            except Exception:  # pylint: disable=broad-except
                try:
                    LOGGER.exception(
                        '[%s] Error executing action "%s" for event %s',
                        event_id,
                        action,
                        event,
                    )
                except Exception:  # pylint: disable=broad-except
                    LOGGER.exception("[%s] Error executing an action")

            if getattr(action, "oneshot", False):
                oneshot_actions.append(action)

        for action in oneshot_actions:
            self.actions[event].remove(action)

        if not suppress_logs:
            LOGGER.debug("[%s] Finished firing event %s", event_id, event)

        self.extra_info[event]["last_finished"] = time.time()
        self.extra_info[event]["last_duration"] = time.time() - start_time

    def _unregister_event(self, event: str, source: str) -> bool:
        suppress_logs = _suppress_logs(event)
        if not suppress_logs:
            LOGGER.debug(
                "Unregistering event %s from source %s", event, source
            )

        if event not in self.events:
            LOGGER.error("Attempt to unregister unknown event %s", event)
            return False

        if source not in self.events[event]:
            LOGGER.error(
                "Attempt to unregister unknown source %s from event %s",
                source,
                event,
            )
            return False

        self.events[event] -= {source}
        if not self.events[event]:
            del self.events[event]

        return True

    def unregister_event(self, event: str, source: str) -> None:
        """Unregister an event from a source"""
        if self._unregister_event(event, source):
            self.unregister_source(source, force=None)

    def unregister_source(
        self,
        source: str,
        *,
        force: Optional[bool] = False,
    ) -> None:
        """Unregister an event source

        Args:
            force: If True, unregisters a source even when it still has
                events associated.
        """
        LOGGER.debug(
            "Removing source %s%s", source, " with force" if force else ""
        )

        events = self.get_events_by_source(source)
        if events:
            if force is False:
                LOGGER.error(
                    "Attempt to unregister source %s,"
                    " which is used for %d events: %s",
                    source,
                    len(events),
                    ", ".join(events),
                )
            if not force:
                return

            for ev in events:
                self._unregister_event(ev, source)

        if source in self.sources:
            self.sources.remove(source)

    def register_action(
        self,
        event: str,
        action: RegistrableAction,
        *,
        oneshot: bool = False,
    ) -> None:
        """Register an action to execute when the ``event`` fires

        Args:
            oneshot: Only execute the action once and remove it afterwards
        """
        action_obj: Optional[ActionCallable]
        if isinstance(action, str):
            action_obj = doorpi.actions.from_string(action)
        elif callable(action):
            action_obj = action
        else:
            raise ValueError("action must be a str or callable")

        if action_obj is None:
            return

        self.actions.setdefault(event, []).append(action_obj)

        LOGGER.trace("Registered action %s for event %s", action, event)

    __call__ = fire_event

def _suppress_logs(event_name: str) -> bool:
    return "OnTime" in event_name


"""
homeassistant.components.sensor.forecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Forecast.io weather service.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.forecast/
"""
import logging
from datetime import timedelta

from homeassistant.util import Throttle
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS)
from homeassistant.helpers.entity import Entity

REQUIREMENTS = ['python-forecastio==1.3.3']
_LOGGER = logging.getLogger(__name__)

# Sensor types are defined like so:
# Name, si unit, us unit, ca unit, uk unit, uk2 unit
SENSOR_TYPES = {
    'summary': ['Summary', '', '', '', '', ''],
    'icon': ['Icon', '', '', '', '', ''],
    'nearest_storm_distance': ['Nearest Storm Distance',
                               'km', 'm', 'km', 'km', 'm'],
    'nearest_storm_bearing': ['Nearest Storm Bearing',
                              '°', '°', '°', '°', '°'],
    'precip_type': ['Precip', '', '', '', '', ''],
    'precip_intensity': ['Precip Intensity', 'mm', 'in', 'mm', 'mm', 'mm'],
    'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%'],
    'temperature': ['Temperature', '°C', '°F', '°C', '°C', '°C'],
    'apparent_temperature': ['Apparent Temperature',
                             '°C', '°F', '°C', '°C', '°C'],
    'dew_point': ['Dew point', '°C', '°F', '°C', '°C', '°C'],
    'wind_speed': ['Wind Speed', 'm/s', 'mph', 'km/h', 'mph', 'mph'],
    'wind_bearing': ['Wind Bearing', '°', '°', '°', '°', '°'],
    'cloud_cover': ['Cloud Coverage', '%', '%', '%', '%', '%'],
    'humidity': ['Humidity', '%', '%', '%', '%', '%'],
    'pressure': ['Pressure', 'mBar', 'mBar', 'mBar', 'mBar', 'mBar'],
    'visibility': ['Visibility', 'km', 'm', 'km', 'km', 'm'],
    'ozone': ['Ozone', 'DU', 'DU', 'DU', 'DU', 'DU'],
}

# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)

def setup_platform(hass, config, add_devices, discovery_info=None):
    """ Get the Forecast.io sensor. """
    import forecastio

    if None in (hass.config.latitude, hass.config.longitude):
        _LOGGER.error("Latitude or longitude not set in Home Assistant config")
        return False

    try:
        forecast = forecastio.load_forecast(config.get(CONF_API_KEY, None),
                                            hass.config.latitude,
                                            hass.config.longitude)
        forecast.currently()
    except ValueError:
        _LOGGER.error(
            "Connection error "
            "Please check your settings for Forecast.io.")
        return False

    if 'units' in config:
        units = config['units']
    elif hass.config.temperature_unit == TEMP_CELCIUS:
        units = 'si'
    else:
        units = 'us'

    data = ForeCastData(config.get(CONF_API_KEY, None),
                        hass.config.latitude,
                        hass.config.longitude,
                        units)

    dev = []
    for variable in config['monitored_conditions']:
        if variable not in SENSOR_TYPES:
            _LOGGER.error('Sensor type: "%s" does not exist', variable)
        else:
            dev.append(ForeCastSensor(data, variable))

    add_devices(dev)

# pylint: disable=too-few-public-methods
class ForeCastSensor(Entity):
    """ Implements an Forecast.io sensor. """

    def __init__(self, weather_data, sensor_type):
        self.client_name = 'Weather'
        self._name = SENSOR_TYPES[sensor_type][0]
        self.forecast_client = weather_data
        self.type = sensor_type
        self._state = None
        self._unit_system = self.forecast_client.unit_system
        if self._unit_system == 'si':
            self._unit_of_measurement = SENSOR_TYPES[self.type][1]
        elif self._unit_system == 'us':
            self._unit_of_measurement = SENSOR_TYPES[self.type][2]
        elif self._unit_system == 'ca':
            self._unit_of_measurement = SENSOR_TYPES[self.type][3]
        elif self._unit_system == 'uk':
            self._unit_of_measurement = SENSOR_TYPES[self.type][4]
        elif self._unit_system == 'uk2':
            self._unit_of_measurement = SENSOR_TYPES[self.type][5]
        self.update()

    @property
    def name(self):
        return '{} {}'.format(self.client_name, self._name)

    @property
    def state(self):
        """ Returns the state of the device. """
        return self._state

    @property
    def unit_of_measurement(self):
        """ Unit of measurement of this entity, if any. """
        return self._unit_of_measurement

    @property
    def unit_system(self):
        """ Unit system of this entity. """
        return self._unit_system

    # pylint: disable=too-many-branches
    def update(self):
        """ Gets the latest data from Forecast.io and updates the states. """
        import forecastio

        self.forecast_client.update()
        data = self.forecast_client.data

        try:
            if self.type == 'summary':
                self._state = data.summary
            elif self.type == 'icon':
                self._state = data.icon
            elif self.type == 'nearest_storm_distance':
                self._state = data.nearestStormDistance
            elif self.type == 'nearest_storm_bearing':
                self._state = data.nearestStormBearing
            elif self.type == 'precip_intensity':
                self._state = data.precipIntensity
            elif self.type == 'precip_type':
                self._state = data.precipType
            elif self.type == 'precip_probability':
                self._state = round(data.precipProbability * 100, 1)
            elif self.type == 'dew_point':
                self._state = round(data.dewPoint, 1)
            elif self.type == 'temperature':
                self._state = round(data.temperature, 1)
            elif self.type == 'apparent_temperature':
                self._state = round(data.apparentTemperature, 1)
            elif self.type == 'wind_speed':
                self._state = data.windSpeed
            elif self.type == 'wind_bearing':
                self._state = data.windBearing
            elif self.type == 'cloud_cover':
                self._state = round(data.cloudCover * 100, 1)
            elif self.type == 'humidity':
                self._state = round(data.humidity * 100, 1)
            elif self.type == 'pressure':
                self._state = round(data.pressure, 1)
            elif self.type == 'visibility':
                self._state = data.visibility
            elif self.type == 'ozone':
                self._state = round(data.ozone, 1)

        except forecastio.utils.PropertyUnavailable:
            pass

class ForeCastData(object):
    """ Gets the latest data from Forecast.io. """

    def __init__(self, api_key, latitude, longitude, units):
        self._api_key = api_key
        self.latitude = latitude
        self.longitude = longitude
        self.data = None
        self.unit_system = None
        self.units = units
        self.update()

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def update(self):
        """ Gets the latest data from Forecast.io. """
        import forecastio

        forecast = forecastio.load_forecast(self._api_key,
                                            self.latitude,
                                            self.longitude,
                                            units=self.units)
        self.data = forecast.currently()
        self.unit_system = forecast.json['flags']['units']


#Deep Cross Entropy
#https://colab.research.google.com/github/Cheukting/rl_workshop/blob/master/exercises/rl_workshop_deep_crossentropy_method.ipynb
'''
Instead of using a table (like for cross entropy use a function)
Start with random uniform probability for policy
loop:
    evaluate policy (using neural network - input:state out:action probabilities) n-times
    get top p% of evaluations and store the states and actions
    Use states and actions to adjust the neural network (so that it is more likely to take top_scorer actions from given states)
'''

import gym
import time
import torch
import torch.nn as nn
import torch.nn.functional as f
import random
import numpy as np
import matplotlib.pyplot as plt
from torch.nn.modules.loss import BCELoss, CrossEntropyLoss

class Network(nn.Module):
    def __init__(self, state_vars, actions):
        super(Network, self).__init__()
        self.layer1 = nn.Linear(in_features=state_vars, out_features=20)
        self.tanh = nn.Tanh()
        self.layer5 = nn.Linear(in_features=20, out_features=actions)

    def forward(self, x):
        x = self.tanh(self.layer1(x))
        x = self.tanh(self.layer5(x))
        x = f.softmax(x, dim=0)
        return x

def run_session(policy):
    states = []
    actions = []
    total_reward = 0

    done = False
    obs = env.reset()
    for _ in range(10000):
        probabilities = policy(torch.tensor(obs, dtype=torch.float)).detach().numpy() #use network to get action probabilities
        action = np.random.choice(np.arange(n_actions), p=probabilities) #Make a choice

        new_obs, reward, done, info = env.step(action)
        total_reward += reward

        states.append(obs)
        actions.append(action)

        obs = new_obs

        if done:
            break
    #print(total_reward)
    return states, actions, total_reward

def run_n_sessions(n_sessions, policy, percentile):

    #Track the generation
    n_state_list = []
    n_action_list = []
    n_total_rewards = []

    #Sample many times from the policy distribution and store the results
    for _ in range(n_sessions):
        obs_list, action_list, total_reward = run_session(policy)
        n_state_list.append(obs_list)
        n_action_list.append(action_list)
        n_total_rewards.append(total_reward)

    #Get best (state, action) pairs index
    reward_cutoff = np.percentile(n_total_rewards, percentile)
    sessions = np.argwhere(np.array(n_total_rewards) >= reward_cutoff).flatten()
    #print(top_scorers)

    top_states = []
    top_actions = []

    #for each top scorer store the states and the actions from those states that were taken
    for session in sessions:
        top_states += [state for state in n_state_list[session]]
        top_actions += [action for action in n_action_list[session]]

    #calculate average reward for generation
    gen_avg_reward = sum(n_total_rewards)/len(n_total_rewards)

    return top_states, top_actions, gen_avg_reward, reward_cutoff

def train(iterations, a_n, policy):

    x = []
    y = []

    n_sessions=100
    percentile=70
    learning_rate=0.001
    loss_fn = BCELoss()

    for i in range(iterations):
        s, a, r, cutoff = run_n_sessions(n_sessions, policy, percentile)

        #For plotting
        y.append(r)
        x.append(i)

        #Set up data for forward and back pass
        a = f.one_hot(torch.LongTensor(a), num_classes=a_n).float()
        s = torch.FloatTensor(s)

        #Use top_scorers (state, actions) to train the network
        for run in zip(s, a):
            pred = policy(run[0])
            target = run[1]
            loss = loss_fn(pred, target)
            policy.zero_grad()
            loss.backward()
            with torch.no_grad():
                for param in policy.parameters():
                    param -= learning_rate * param.grad

        print('Iteration: {} | Avg Reward: {} | Cutoff: {}'.format(i, round(r, 2), cutoff))

    return x, y, policy

def plot(x, y):
    plt.plot(x, y)
    plt.xlabel('Iterations')
    plt.ylabel('Average Reward')
    plt.show()

def test_run(version):
    done = False
    obs = env.reset()
    total_reward = 0
    for _ in range(500):
        env.render()
        probabilities = policy(torch.tensor(obs, dtype=torch.float)).detach().numpy() #use network to get action probabilities
        action = np.random.choice(np.arange(n_actions), p=probabilities) #Make a choice

        new_obs, reward, done, info = env.step(action)
        total_reward += reward

        obs = new_obs

        if done:
            break
    env.close()
    print('{} Total Reward: {}'.format(version, total_reward))

#Initialize
env_name = 'CartPole-v0'
env = gym.make(env_name).env
n_actions = env.action_space.n #number of actions
n_states = len(env.observation_space.sample()) #number of aspects for state
policy = Network(state_vars=n_states, actions=n_actions)

#Run
test_run('Pre-Training')
x, y, policy = train(100, n_actions, policy)
plot(x, y)
test_run('Post-Training')


from copy import deepcopy

import torch
import torch.nn as nn
from torch.nn.parallel.distributed import _find_tensors

from ..builder import MODELS, build_module
from ..common import set_requires_grad
from .base_gan import BaseGAN

# _SUPPORT_METHODS_ = ['DCGAN', 'STYLEGANv2']

# @MODELS.register_module(_SUPPORT_METHODS_)
@MODELS.register_module()
class StaticUnconditionalGAN(BaseGAN):
    """Unconditional GANs with static architecture in training.

    This is the standard GAN model containing standard adversarial training
    schedule. To fulfill the requirements of various GAN algorithms,
    ``disc_auxiliary_loss`` and ``gen_auxiliary_loss`` are provided to
    customize auxiliary losses, e.g., gradient penalty loss, and discriminator
    shift loss. In addition, ``train_cfg`` and ``test_cfg`` aims at setuping
    training schedule.

    Args:
        generator (dict): Config for generator.
        discriminator (dict): Config for discriminator.
        gan_loss (dict): Config for generative adversarial loss.
        disc_auxiliary_loss (dict): Config for auxiliary loss to
            discriminator.
        gen_auxiliary_loss (dict | None, optional): Config for auxiliary loss
            to generator. Defaults to None.
        train_cfg (dict | None, optional): Config for training schedule.
            Defaults to None.
        test_cfg (dict | None, optional): Config for testing schedule. Defaults
            to None.
    """

    def __init__(self,
                 generator,
                 discriminator,
                 gan_loss,
                 disc_auxiliary_loss=None,
                 gen_auxiliary_loss=None,
                 train_cfg=None,
                 test_cfg=None):
        """ 最基礎的GAN模型
        Args:
            generator: 生成器相關配置設定
            discriminator: 鑑別器相關配置設定
            gan_loss: 損失計算相關配置設定
            disc_auxiliary_loss: 鑑別器的輔助訓練設定
            gen_auxiliary_loss: 生成器的輔助訓練設定
            train_cfg: 訓練模式下的流程設定
            test_cfg: 測試模式下的流程設定
        """
        # 繼承自BaseGAN，對繼承對象進行初始化
        super().__init__()
        # 深拷貝一份生成器的config資料到self當中
        self._gen_cfg = deepcopy(generator)
        # 實例化生成器實例對象
        self.generator = build_module(generator)

        # 在驗證模式下可以不用指定鑑別器，因為我們也不會需要用到
        # support no discriminator in testing
        if discriminator is not None:
            # 實例化鑑別器實例對象
            self.discriminator = build_module(discriminator)
        else:
            # 如果沒有指定配置文件就會是None
            self.discriminator = None

        # support no gan_loss in testing
        # 在測試模式下可以不用構建損失計算方式
        if gan_loss is not None:
            # 如果有指定損失計算方式就會到這裡進行實例化
            self.gan_loss = build_module(gan_loss)
        else:
            # 如果沒有指定就會是None
            self.gan_loss = None

        if disc_auxiliary_loss:
            # 如果有使用鑑別器輔助訓練就會到這裡進行實例化
            self.disc_auxiliary_losses = build_module(disc_auxiliary_loss)
            if not isinstance(self.disc_auxiliary_losses, nn.ModuleList):
                # 因為輔助方始可能會有多個，所以會用ModuleList進行包裝
                self.disc_auxiliary_losses = nn.ModuleList(
                    [self.disc_auxiliary_losses])
        else:
            # 如果沒有指定就會設定成None
            self.disc_auxiliary_loss = None

        if gen_auxiliary_loss:
            # 如果有使用生成器輔助訓練就會到這裡進行實例化
            self.gen_auxiliary_losses = build_module(gen_auxiliary_loss)
            if not isinstance(self.gen_auxiliary_losses, nn.ModuleList):
                # 因為輔助方始可能會有多個，所以會用ModuleList進行包裝
                self.gen_auxiliary_losses = nn.ModuleList([self.gen_auxiliary_losses])
        else:
            # 如果沒有指定就會設定成None
            self.gen_auxiliary_losses = None

        # 拷貝train以及test的config資料
        self.train_cfg = deepcopy(train_cfg) if train_cfg else None
        self.test_cfg = deepcopy(test_cfg) if test_cfg else None

        # 解析train_cfg資料
        self._parse_train_cfg()
        if test_cfg is not None:
            # 如果有設定test_cfg就進行解析
            self._parse_test_cfg()

    def _parse_train_cfg(self):
        """Parsing train config and set some attributes for training."""
        # 解析train_cfg當中資料
        if self.train_cfg is None:
            # 如果train_cfg是None就構建一個空的dict
            self.train_cfg = dict()
        # control the work flow in train step
        # 獲取train_cfg當中的disc_steps資料，如果沒有就是預設為1
        self.disc_steps = self.train_cfg.get('disc_steps', 1)

        # whether to use exponential moving average for training
        # 獲取train_cfg當中的use_ema資料，如果沒有就會是False
        self.use_ema = self.train_cfg.get('use_ema', False)
        if self.use_ema:
            # 如果使用ema就會到這裡
            # use deepcopy to guarantee the consistency
            self.generator_ema = deepcopy(self.generator)

        # 獲取real_img_key資料
        self.real_img_key = self.train_cfg.get('real_img_key', 'real_img')

    def _parse_test_cfg(self):
        """Parsing test config and set some attributes for testing."""
        if self.test_cfg is None:
            self.test_cfg = dict()

        # basic testing information
        self.batch_size = self.test_cfg.get('batch_size', 1)

        # whether to use exponential moving average for testing
        self.use_ema = self.test_cfg.get('use_ema', False)
        # TODO: finish ema part

    def train_step(self,
                   data_batch,
                   optimizer,
                   ddp_reducer=None,
                   loss_scaler=None,
                   use_apex_amp=False,
                   running_status=None):
        """Train step function.

        This function implements the standard training iteration for
        asynchronous adversarial training. Namely, in each iteration, we first
        update discriminator and then compute loss for generator with the newly
        updated discriminator.

        As for distributed training, we use the ``reducer`` from ddp to
        synchronize the necessary params in current computational graph.

        Args:
            data_batch (dict): Input data from dataloader.
            optimizer (dict): Dict contains optimizer for generator and
                discriminator.
            ddp_reducer (:obj:`Reducer` | None, optional): Reducer from ddp.
                It is used to prepare for ``backward()`` in ddp. Defaults to
                None.
            loss_scaler (:obj:`torch.cuda.amp.GradScaler` | None, optional):
                The loss/gradient scaler used for auto mixed-precision
                training. Defaults to ``None``.
            use_apex_amp (bool, optional). Whether to use apex.amp. Defaults to
                ``False``.
            running_status (dict | None, optional): Contains necessary basic
                information for training, e.g., iteration number. Defaults to
                None.

        Returns:
            dict: Contains 'log_vars', 'num_samples', and 'results'.
        """
        # get data from data_batch
        # 獲取真實圖像資料，tensor shape [batch_size, channel, height, width]
        real_imgs = data_batch[self.real_img_key]
        # If you adopt ddp, this batch size is local batch size for each GPU.
        # If you adopt dp, this batch size is the global batch size as usual.
        # 獲取圖像的batch_size
        batch_size = real_imgs.shape[0]

        # get running status
        if running_status is not None:
            # 如果有傳入running_status就會獲取當前是第幾個batch
            curr_iter = running_status['iteration']
        else:
            # 如果沒有給定running_status就自己瞎搞一個
            # dirty walk round for not providing running status
            if not hasattr(self, 'iteration'):
                self.iteration = 0
            curr_iter = self.iteration

        # disc training
        # 將self.discriminator的反向傳遞開啟
        set_requires_grad(self.discriminator, True)
        # 將鑑別器的優化器進行反向清空
        optimizer['discriminator'].zero_grad()
        # TODO: add noise sampler to customize noise sampling

        # pass model specific training kwargs
        # 保存生成器的相關參數
        g_training_kwargs = {}
        if hasattr(self.generator, 'get_training_kwargs'):
            # 如果在generator當中有get_training_kwargs就會到這裡，將資料放到g_training_kwargs當中
            g_training_kwargs.update(self.generator.get_training_kwargs(phase='disc'))

        with torch.no_grad():
            # 進行生成圖像，這裡會將反向傳遞進行關閉，fake_imgs = tensor shape [batch_size, channel, height, width]
            fake_imgs = self.generator(None, num_batches=batch_size, **g_training_kwargs)

        # disc pred for fake imgs and real_imgs
        # 將假的圖像放到鑑別器模型當中進行正向推理，disc_pred_fake = tensor shape [batch_size, channel=1]
        disc_pred_fake = self.discriminator(fake_imgs)
        # 將資料集的圖像放到鑑別器當中進行正向推理，disc_pred_real = tensor shape [batch_size, channel=1]
        disc_pred_real = self.discriminator(real_imgs)
        # get data dict to compute losses for disc
        # 構建一個data_dict_用來計算損失用的
        data_dict_ = dict(
            # 將生成器模型
            gen=self.generator,
            # 將鑑別器模型
            disc=self.discriminator,
            # 預測生成圖結果資料
            disc_pred_fake=disc_pred_fake,
            # 預測真實圖結果資料
            disc_pred_real=disc_pred_real,
            # 假圖資料
            fake_imgs=fake_imgs,
            # 真圖資料
            real_imgs=real_imgs,
            # 當前是第幾個batch
            iteration=curr_iter,
            # batch大小
            batch_size=batch_size,
            loss_scaler=loss_scaler)

        # 將data_dict_傳入進行計算損失值
        # loss_disc = 損失總和，tensor float
        # log_vars_disc = 個別損失資料，dict
        loss_disc, log_vars_disc = self._get_disc_loss(data_dict_)

        # prepare for backward in ddp. If you do not call this function before
        # back propagation, the ddp will not dynamically find the used params
        # in current computation.
        if ddp_reducer is not None:
            # 準備進行反向傳遞，在使用分布式訓練時會需要的動作
            ddp_reducer.prepare_for_backward(_find_tensors(loss_disc))

        if loss_scaler:
            # add support for fp16
            loss_scaler.scale(loss_disc).backward()
        elif use_apex_amp:
            from apex import amp
            with amp.scale_loss(
                    loss_disc, optimizer['discriminator'],
                    loss_id=0) as scaled_loss_disc:
                scaled_loss_disc.backward()
        else:
            # 進行loss反向傳遞
            loss_disc.backward()

        if loss_scaler:
            loss_scaler.unscale_(optimizer['discriminator'])
            # note that we do not contain clip_grad procedure
            loss_scaler.step(optimizer['discriminator'])
            # loss_scaler.update will be called in runner.train()
        else:
            # 將優化器進行step操作，到這裡就算將鑑別器進行一次訓練
            optimizer['discriminator'].step()

        # skip generator training if only train discriminator for current iteration
        if (curr_iter + 1) % self.disc_steps != 0:
            # 可以透過設定disc_steps讓某幾個batch只會訓練鑑別器
            # 構建results，裡面就會有創建的假圖以及真圖
            results = dict(fake_imgs=fake_imgs.cpu(), real_imgs=real_imgs.cpu())
            # 構建最終回傳的資料
            outputs = dict(
                log_vars=log_vars_disc,
                num_samples=batch_size,
                results=results)
            if hasattr(self, 'iteration'):
                # 將iteration加一
                self.iteration += 1
            # 最終回傳結果
            return outputs

        # generator training
        # 將鑑別器的反向傳遞關閉
        set_requires_grad(self.discriminator, False)
        # 將生成器的優化器反向傳遞值進行清空
        optimizer['generator'].zero_grad()

        # TODO: add noise sampler to customize noise sampling

        # pass model specific training kwargs
        g_training_kwargs = {}
        if hasattr(self.generator, 'get_training_kwargs'):
            # 如果generator當中有get_trainging_kwargs就會存到g_training_kwargs當中
            g_training_kwargs.update(self.generator.get_training_kwargs(phase='gen'))

        # 透過generator創造出假圖，tensor shape [batch_size, channel, height, width]
        fake_imgs = self.generator(None, num_batches=batch_size, **g_training_kwargs)
        # 對假圖進行預測
        disc_pred_fake_g = self.discriminator(fake_imgs)

        # 構建計算損失需要使用的字典
        data_dict_ = dict(
            # 生成器模型
            gen=self.generator,
            # 鑑別器模型
            disc=self.discriminator,
            # 假圖資料
            fake_imgs=fake_imgs,
            # 鑑別器辨識結果
            disc_pred_fake_g=disc_pred_fake_g,
            # 當前第幾個batch
            iteration=curr_iter,
            # batch大小
            batch_size=batch_size,
            loss_scaler=loss_scaler)

        # 進行損失計算
        loss_gen, log_vars_g = self._get_gen_loss(data_dict_)

        # prepare for backward in ddp. If you do not call this function before
        # back propagation, the ddp will not dynamically find the used params
        # in current computation.
        if ddp_reducer is not None:
            # 如果在分布式訓練上會需要
            ddp_reducer.prepare_for_backward(_find_tensors(loss_gen))

        if loss_scaler:
            loss_scaler.scale(loss_gen).backward()
        elif use_apex_amp:
            from apex import amp
            with amp.scale_loss(
                    loss_gen, optimizer['generator'],
                    loss_id=1) as scaled_loss_disc:
                scaled_loss_disc.backward()
        else:
            # 進行損失的反向傳遞
            loss_gen.backward()

        if loss_scaler:
            loss_scaler.unscale_(optimizer['generator'])
            # note that we do not contain clip_grad procedure
            loss_scaler.step(optimizer['generator'])
            # loss_scaler.update will be called in runner.train()
        else:
            # 透過優化器進行權重更新
            optimizer['generator'].step()

        # update ada p
        if hasattr(self.discriminator, 'with_ada') and self.discriminator.with_ada:
            # 如果discriminator有with_ada就會到這裡
            self.discriminator.ada_aug.log_buffer[0] += batch_size
            self.discriminator.ada_aug.log_buffer[1] += disc_pred_real.sign().sum()
            self.discriminator.ada_aug.update(iteration=curr_iter, num_batches=batch_size)
            log_vars_disc['augment'] = (self.discriminator.ada_aug.aug_pipeline.p.data.cpu())

        # 構建log所需的字典
        log_vars = {}
        # 生成器的損失資訊
        log_vars.update(log_vars_g)
        # 鑑別器的損失資訊
        log_vars.update(log_vars_disc)

        # 最終的results資訊，這裡會有假圖以及真圖的資訊
        results = dict(fake_imgs=fake_imgs.cpu(), real_imgs=real_imgs.cpu())
        # 將資料用dict包裝其來放到outputs當中
        outputs = dict(log_vars=log_vars, num_samples=batch_size, results=results)

        if hasattr(self, 'iteration'):
            # 更新iteration
            self.iteration += 1
        # 回傳outputs
        return outputs


import torch
import numpy as np

def reservoir(seen_examples, buffer_size):
    if seen_examples < buffer_size:
        return seen_examples

    rand = np.random.randint(0, seen_examples + 1)
    if rand < buffer_size:
        return rand
    else:
        return -1

class Buffer:
    def __init__(self, buffer_size, device):
        self.buffer_size = buffer_size
        self.device = device
        self.seen_examples = 0
        self.attributes = ['examples', 'labels']
        self.examples = [None] * self.buffer_size
        self.labels = [None] * self.buffer_size
        self.logits = [None] * self.buffer_size
        self.task_number = [None] * self.buffer_size

    def add_data(self, examples, task=None, labels=None,logits=None):
        for i in range(examples.shape[0]):
            index = reservoir(self.seen_examples, self.buffer_size)
            self.seen_examples += 1
            if index >= 0:
                self.examples[index] = examples[i].to(self.device)
                self.task_number[index] = task
                if labels is not None:
                    self.labels[index] = labels[i].to(self.device)
                if logits is not None:
                    self.logits[index] = logits[i].to(self.device)

    def get_data(self, size,task_labels=False):
        if size > min(self.seen_examples, len([x for x in self.examples if x is not None])):
            size = min(self.seen_examples, len([x for x in self.examples if x is not None]))

        choice = np.random.choice(min(self.seen_examples, len([x for x in self.examples if x is not None])),
                                  size=size, replace=False)

        ret_examples = [self.examples[idx] for idx in choice]
        ret_labels = [self.labels[idx] for idx in choice]
        ret_logits = [self.logits[idx] for idx in choice]

        if task_labels:
            ret_task_labels = [self.task_number[idx] for idx in choice]
            return ret_examples, ret_labels, ret_task_labels

        return ret_examples, ret_labels, ret_logits

    def is_empty(self):
        if self.seen_examples == 0:
            return True
        else:
            return False

    def get_all_data(self):
        ret_tuple = (torch.stack([ee.cpu()
                                  for ee in self.examples]).to(self.device),)
        for attr_str in self.attributes[1:]:
            if hasattr(self, attr_str):
                attr = getattr(self, attr_str)
                ret_tuple += (attr,)

        return ret_tuple

    def clear(self):
        for attr_str in self.attributes:
            if hasattr(self, attr_str):
                delattr(self, attr_str)
        self.seen_examples = 0


# to allow running from terminal without pyCharm :(
import sys

sys.path.append('../ope')
sys.path.append('../models/q_learning')
sys.path.append('../models/attention/nn')
sys.path.append('../models/ckconv/nn')
sys.path.append('../models')

import torch
import argparse
import pandas as pd
from q_learning import DQN, fit_double_dqn
from baseline_encoders import *
from utils import load_data, count_parameters, OPECallback, add_intermediate_reward

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Trains policy with pretrained encoder')
    parser.add_argument('-d', "--dataset", type=str, default='amsterdam-umc-db')
    parser.add_argument('-m', "--baseline", type=str, default='last_state')
    parser.add_argument('-o', "--out_dims", type=int, default=96)
    parser.add_argument('-a', "--alpha", type=float, default=1e-4)
    parser.add_argument('-g', "--gamma", type=float, default=0.95)
    parser.add_argument('-l', "--tau", type=float, default=1e-4)
    parser.add_argument('-e', "--episodes", type=int, default=200000)
    parser.add_argument('-k', "--eval_after", type=int, default=5000)
    parser.add_argument('-z', "--freeze_encoder", type=bool, default=False)
    parser.add_argument('-w', "--replay_alpha", type=float, default=0.6)
    parser.add_argument('-q', "--replay_beta", type=float, default=0.9)
    parser.add_argument('-b', "--batch_size", type=int, default=32)
    args = vars(parser.parse_args())
    print('Running with args:', args)

    behavior_policy_file = '../ope/physician_policy/%s_aggregated_full_cohort_2h_mlp/valid_behavior_policy.csv' % args['dataset']
    train_df = load_data('../preprocessing/datasets/%s/aggregated_full_cohort_2h/train.csv' % args['dataset'])
    valid_df = load_data('../preprocessing/datasets/%s/aggregated_full_cohort_2h/valid.csv' % args['dataset'])

    # Add intermediate rewards based on MAP and lactate
    train_df.reward = add_intermediate_reward(train_df)
    valid_df.reward = add_intermediate_reward(valid_df)

    in_channels = train_df.filter(regex='x\d+').shape[1]

    if args['baseline'] == 'last_state':
        encoder = LastState(in_channels, args['out_dims'])
    elif args['baseline'] == 'concat-2':
        encoder = StateConcatenation(in_channels, args['out_dims'], k=2)
    elif args['baseline'] == 'concat-3':
        encoder = StateConcatenation(in_channels, args['out_dims'], k=3)
    elif args['baseline'] == 'tcn':
        encoder = TCN((in_channels, 64, args['out_dims']), kernel_sizes=(18, 3), dilations=(2, 1))
    elif args['baseline'] == 'lstm':
        encoder = LSTM(in_channels, args['out_dims'])
    elif args['baseline'] == 'gru':
        encoder = GRU(in_channels, args['out_dims'])
    else:
        raise Exception('Method %s not recognized' % args['baseline'])
    print('%s parameters: %d' % (args['baseline'], count_parameters(encoder)))

    dqn = DQN(state_dim=args['out_dims'], hidden_dims=(96, 96,), num_actions=25, disallowed_actions=(1, 2, 3, 4))
    print('DQN parameters:  ', count_parameters(dqn))

    # intermittently evaluate policy
    callback = OPECallback(behavior_policy_file=behavior_policy_file, valid_data=valid_df)

    # Fit!
    fit_double_dqn(
        experiment='results/%s/%s_experiment' % (args['dataset'], args['baseline']),
        policy=dqn,
        encoder=encoder,
        dataset=train_df,
        lrate=args['alpha'],
        gamma=args['gamma'],
        tau=args['tau'],
        lambda_reward=1,
        num_episodes=args['episodes'],
        batch_size=args['batch_size'],
        freeze_encoder=args['freeze_encoder'],  # set freeze_encoder=True to disable fine-tuning!
        eval_func=callback,
        eval_after=args['eval_after'],
        scheduler_gamma=0.95,
        replay_alpha=args['replay_alpha'],
        replay_beta=args['replay_beta'],
        step_scheduler_after=10000,
        min_max_reward=(-27, 10),
        save_on=None
    )


import game_of_life_interface
import numpy as np
import matplotlib.pyplot as plt

class GameOfLife(game_of_life_interface.GameOfLife):  # This is the way you construct a class that heritage properties

    def __init__ (self, size_of_board, board_start_mode, rules, rle, pattern_position) :  # constructor
        # declare and initialize variables
        self.size_of_board = size_of_board
        self.board_start_mode = board_start_mode
        self.board = np.zeros ((size_of_board, size_of_board))
        self.rules = rules
        self.B = list (map (int, rules.split ("/") [ 0 ] [ 1 : ]))
        self.S = list (map (int, rules.split ("/") [ 1 ] [ 1 : ]))
        self.pattern_position = pattern_position
        self.rle = rle
        self.alive = 255
        self.dead = 0
        if board_start_mode == 1 and self.rle == "" :

            self.board = np.random.choice ([ 0, 255 ], (size_of_board, size_of_board), p=[ 0.5, 0.5 ]).reshape (
                self.size_of_board, self.size_of_board)
        elif board_start_mode == 2 and self.rle == "" :
            self.board = np.random.choice ([ 255, 0 ], (size_of_board, size_of_board), p=[ 0.8, 0.2 ]).reshape (
                self.size_of_board, self.size_of_board)

        elif board_start_mode == 3 and self.rle == "" :
            self.board = np.random.choice ([ 0, 255 ], (size_of_board, size_of_board), p=[ 0.2, 0.8 ]).reshape (
                self.size_of_board, self.size_of_board)

        elif board_start_mode == 4 and self.rle == "" :  # glider gun board
            self.board = np.zeros ((self.size_of_board, self.size_of_board), dtype=int)
            self.board [ 14, 11 ] = 255
            self.board [ 15, 11 ] = 255
            self.board [ 14, 10 ] = 255
            self.board [ 15, 10 ] = 255
            self.board [ 14, 20 ] = 255
            self.board [ 15, 20 ] = 255
            self.board [ 16, 20 ] = 255
            self.board [ 13, 21 ] = 255
            self.board [ 17, 21 ] = 255
            self.board [ 12, 22 ] = 255
            self.board [ 18, 22 ] = 255
            self.board [ 12, 23 ] = 255
            self.board [ 18, 23 ] = 255
            self.board [ 15, 24 ] = 255
            self.board [ 13, 25 ] = 255
            self.board [ 17, 25 ] = 255
            self.board [ 14 :17, 26 ] = 255
            self.board [ 15, 27 ] = 255
            self.board [ 12 :15, 30 ] = 255
            self.board [ 12 :15, 31 ] = 255
            self.board [ 11, 32 ] = 255
            self.board [ 15, 32 ] = 255
            self.board [ 10 :12, 34 ] = 255
            self.board [ 15 :17, 34 ] = 255
            self.board [ 12 :14, 44 ] = 255
            self.board [ 12 :14, 45 ] = 255
        else :
            self.transform_rle_to_matrix (self.rle)
    def if_check (self, k, m, board) :#checks the index in some board insert returns true or false
        return 1 if board [ k ] [ m ] == self.alive else 0
    def check_neighbours (self,i, j,current_board) :#checks the neighbors val according to the rules
        count = 0
        size = self.size_of_board
        count = count + self.if_check((i - 1) % size, (j - 1) % size,current_board)  # left-upper1
        count = count + self.if_check((i - 1) % size, j,current_board )  # upper1
        count = count + self.if_check((i - 1) % size, (j + 1) % size,current_board)  # right-upper1
        count = count + self.if_check(i , (j - 1) % size,current_board)  # left1
        count = count + self.if_check(i, (j + 1) % size,current_board)  # right1
        count = count + self.if_check((i + 1) % size, (j - 1) % size,current_board)  # left-down1
        count = count + self.if_check((i + 1) % size, j,current_board)  # down1
        count = count + self.if_check((i + 1) % size, (j + 1) % size,current_board)  # right down1
        if (current_board[i][j]==0 and count in self.B) or (current_board[i][j]==255 and count in self.S):
            return True
        else:
            return False
    def copy_board(self):#this function makes a deep copy of a board
        list_to_ret = []
        for i in range (self.size_of_board) :
            temp_list = []
            for j in range (self.size_of_board) :
                temp_list.append(self.board[i][j])
            list_to_ret.append(temp_list)
        return list_to_ret
    def update(self): # update the board based on given rules
        current_board = self.copy_board()
        for i in range(self.size_of_board):
            for j in range(self.size_of_board):
                if self.check_neighbours(i,j,current_board) == True:
                    self.board[i][j] = self.alive # 255
                else:
                    self.board[i][j] = self.dead # 0

        #  wrote many lines to update ,but changed it according to study about modulo.
        #RuleBook - Main interface for game rules
#GOLRuleBook - Regular Game of Life rules implementing RuleBook
#Cell - An interface for a living organism :) Which lives or dies according to the rules in rulebook. A cell can answer are you alive question.
#AliveCell - Represents a living cell
#DeadCell - Represents a dead cell
#Board - The game world. Holds all the cells, and their locations. Can tell each cells number of neighbours.
#Player - The player of the game. Player is just responsible for generation change of the board.
#CellFactory - Responsible for cell creation (Gives the same instance for all the alive cells, and gives the same instance for all the dead cells)
#Populator - Cell generators
#StrLoadingPopulator - Load initial cells from new line delimited string such as [A D A]\n[D D D]
#RandomPopulator - Given number of rows and number of columns randomly generate cells
#Main - Main class. Number of rows, number of columns and number of generations are collecte

    def transform_rle_to_matrix (self, rle) :#this function goes threw the list of rle on the insert and describes it on the board

        z, w = self.pattern_position  # position tupple for w,z axis
        space = ""
        self.board = [ [ 0 for i in range (self.size_of_board) ] for j in range (self.size_of_board) ]

        for i in rle :
            if i != "!" :
                if i != "$" :
                    if str (i).isdigit ( ) :
                        space += i
                    if i == "o" :
                        if space == "" :
                            space = 1
                        for j in range (w, w + int (space)) :
                            self.board [ z ] [ j ] = self.alive
                        w = w + int (space)
                        space = ""
                    if i == "b" :
                        if space == "" :
                            space = 1
                        for j in range (w, w + int (space)) :
                            self.board [ z ] [ j ] = self.dead
                        w = w + int (space)
                        space = ""
                else :
                    if space == "" :
                        space = 1
                    z = z + int (space)
                    space = ""
                    w = self.pattern_position [ 1 ]

        # initialize board based on starting position
    def save_board_to_file(self, file_name):#this function saves the board files each iteration
        plt.imsave(file_name, self.board)
    def display_board(self):#this function make a display of the board
       plt.imshow(self.board)
       plt.pause(0.0001)
    def return_board (self): #this function returns a list of the board position after checking if it is a list
        if type(self.board)==list:
            return self.board
        else:
            return self.board.tolist ( )

if __name__ == '__main__':
    # You should keep this line for our auto-grading code.
    # game = GameOfLife(120 , 4,"b3/s23","","")
    game = GameOfLife (100, 4, "b3/s23",
                       "5b3o11b3o5b$4bo3bo9bo3bo4b$3b2o4bo7bo4b2o3b$2bobob2ob2o5b2ob2obobo2b$b2obo4bob2ob2obo4bob2ob$o4bo3bo2bobo2bo3bo4bo$12bobo12b$2o7b2obobob2o7b2o$12bobo12b$6b3o9b3o6b$6bo3bo9bo6b$6bobo4b3o11b$12bo2bo4b2o5b$15bo11b$11bo3bo11b$11bo3bo11b$15bo11b$12bobo!",
                       (40, 40))
    for i in range (180) :
        print ("updating iteration number " + str (i))
        game.update ( )
        game.display_board ( )

    game = GameOfLife(100 , 4,"b3/s23", "5b3o11b3o5b$4bo3bo9bo3bo4b$3b2o4bo7bo4b2o3b$2bobob2ob2o5b2ob2obobo2b$b2obo4bob2ob2obo4bob2ob$o4bo3bo2bobo2bo3bo4bo$12bobo12b$2o7b2obobob2o7b2o$12bobo12b$6b3o9b3o6b$6bo3bo9bo6b$6bobo4b3o11b$12bo2bo4b2o5b$15bo11b$11bo3bo11b$11bo3bo11b$15bo11b$12bobo!",(40,40))
    # game = GameOfLife(100 , 4,"b3/s23", "5b3o11b3o5b$4bo3bo9bo3bo4b$3b2o4bo7bo4b2o3b$2bobob2ob2o5b2ob2obobo2b$b2obo4bob2ob2obo4bob2ob$o4bo3bo2bobo2bo3bo4bo$12bobo12b$2o7b2obobob2o7b2o$12bobo12b$6b3o9b3o6b$6bo3bo9bo6b$6bobo4b3o11b$12bo2bo4b2o5b$15bo11b$11bo3bo11b$11bo3bo11b$15bo11b$12bobo!",(40,40))


from django.contrib import admin
from django.urls import path
from evotingapp import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.uhome),
    path('uhome/', views.uhome),
    path('ureg/', views.ureg),
    path('uvreg/',views.uvreg),
    path('userv/',views.userv),
    path('ucontact/',views.ucontact),
    path('uabout/',views.uabout),
    path('vot/',views.vot),
    path('voted/',views.voted),
    path('candireg/',views.candireg),
    path('contactaction/',views.contactaction),
    path('adminhome/',views.adminhome),
    path('adminlogin/',views.adminlogin),
    path('partylogin/',views.partylogin),
    path('partyhome/',views.partylogin),
    path('partynews/',views.partynews),
    path('uregaction/',views.uregaction),
    path('uregactionone/',views.uregactionone),
    path('usersaction/',views.usersaction),
    path('adminloginaction/',views.adminloginaction),
    path('partyloginaction/',views.partyloginaction),
    path('uvregaction/',views.uvregaction),
]


"""
Use a simple Discrete Variable Representation method to solve
one-dimensional potentials.

A good general introduction to DVR methods is
Light and Carrington, Adv. Chem. Phys. 114, 263 (2000)
"""

from matplotlib import pyplot as plt
import numpy as np
import scipy.sparse.linalg as sla
import scipy.special.orthogonal as ortho
# import bessel
import warnings

class DVR(object):
    def v(self, V):
        """
        Return the potential matrix with the given potential.
        Usage:
            v_matrix = self.v(V)

        @param[in] V potential function
        @returns v_matrix potential matrix
        """
        v_matrix = np.diag(V(self.x))
        return v_matrix

    def h(self, V):
        """Return the hamiltonian matrix with the given potential.
        Usage:
            H = self.h(V)

        @param[in] V potential function
        @returns H potential matrix
        """
        return self.t() + self.v(V)

    def dvr2fbr(DVR, T):
        """Transform a matrix from the discrete variable representation
        to the finite basis representation"""
        return np.dot(T, np.dot(DVR, np.transpose(T)))

    def fbr2dvr(FBR, T):
        """Transform a matrix from the finite basis representation to the
        discrete variable representation."""
        return np.dot(np.transpose(T), np.dot(FBR, T))

    def plot(self, V, E, U, **kwargs):
        doshow = kwargs.get('doshow', False)
        nplot = kwargs.get('nplot', 5)
        xmin = kwargs.get('xmin', self.x.min())
        xmax = kwargs.get('xmax', self.x.max())
        ymin = kwargs.get('ymin', np.ceil(V(self.x).min() - 1.))
        ymax = kwargs.get('ymax',
                          np.floor(max(U.max()+E.max()+1., V(self.x).max()+1.)))
        plt.plot(self.x, V(self.x))
        for i in range(nplot):
            if i == 0:
                plt.plot(self.x, abs(U[:, i])+E[i])
            else:
                plt.plot(self.x, U[:, i]+E[i])
        plt.axis(ymax=ymax, ymin=ymin)
        plt.axis(xmax=xmax, xmin=xmin)
        if doshow: plt.show()
        return

    def run(self, V, num_eigs = 5, **kwargs):
        h = self.h(V)
        # Get the eigenpairs
        # There are multiple options here.
        # If the user is asking for all of the eigenvalues,
        # then we need to use np.linalg.eigh()
        if num_eigs == h.shape[0]:
            E, U = np.linalg.eigh(h)
        # But if we don't need all eigenvalues, only the smallest ones,
        # then when the size of the H matrix becomes large enough, it is
        # better to use sla.eigsh() with a shift-invert method. Here we
        # have to have a good guess for the smallest eigenvalue so we
        # ask for eigenvalues closest to the minimum of the potential.
        else:
            E, U = sla.eigsh(h, k=num_eigs, which='LM',
                             sigma=V(self.x).min())

        xmin = kwargs.get('xmin', self.x.min())
        xmax = kwargs.get('xmax', self.x.max())
        ymin = kwargs.get('ymin', np.ceil(V(self.x).min() - 1.))
        ymax = kwargs.get('ymax',
                          np.floor(max(U.max()+E.max()+1., V(self.x).max()+1.)))
        precision = kwargs.get('precision', 8)

        # Print and plot stuff
        print('The first {n:d} energies are:'.format(n=num_eigs))
        print(np.array_str(E[:num_eigs], precision=precision))
        self.plot(V, E, U, nplot=num_eigs,
                  xmin=xmin, xmax=xmax,
                  ymin=ymin, ymax=ymax,
                  doshow=True)
        return

    def inf_square_well_test(self, precision=8):
        print('Testing 1-D DVR with an infinite square-well potential')
        vF = VFactory()
        V = vF.square_well(depth=1e30, width=10.)
        self.test_potential(V, num_eigs=5, precision=precision,
                            xmin=-10., xmax=10.,
                            ymin=-0.25, ymax=2.)
        e_exact = np.square(np.arange(1,6)) * np.pi**2. / 2. / 10.**2.
        print("Compare to the exact energies:")
        print(np.array_str(e_exact, precision=precision))
        print
        return

    def square_well_test(self, precision=8):
        #print'Testing 1-D DVR with a finite square-well potential'
        vF = VFactory()
        V = vF.square_well(depth=9./2., width=10.)
        self.test_potential(V, num_eigs=5, precision=precision,
                            xmin=-10., xmax=10.,
                            ymin=-0.25, ymax=2.)
        e_exact = 9./2. * np.array([0.009636, 0.038522, 0.086582,
                                    0.153683, 0.239608])
        print("Compare to these energies:")
        print(np.array_str(e_exact, precision=precision))
        #print "from: http://pilotscholars.up.edu/phy_facpubs/8\n"
        return

    def double_well_test(self, precision=8):
        print('Testing 1-D DVR with a double-well potential')
        vF = VFactory()
        V = vF.double_well()
        self.test_potential(V, num_eigs=5, precision=precision,
                            xmin=-3.5, xmax=3.5,
                            ymin=-0.5, ymax=4.)
        print
        return

    def sho_test(self, k=1., num_eigs=5, precision=8,
            xmin=-3.5, xmax=3.5, ymin=0., ymax=6.):
        print('Testing 1-D DVR with an SHO potential')
        vF = VFactory()
        V = vF.sho(k=k)
        self.run(V, num_eigs=num_eigs,
                            precision=precision,
                            xmin=xmin, xmax=xmax,
                            ymin=ymin, ymax=ymax)
        print
        return

    def morse_test(self, precision=8, xmin=0., xmax=32., ymin=-3., ymax=1.):
        print('Testing 1-D DVR with a Morse potential')
        vF = VFactory()
        V = vF.morse(D=3., a=0.5)
        self.test_potential(V, num_eigs=5, precision=precision,
                            xmin=xmin, xmax=xmax,
                            ymin=ymin, ymax=ymax)
        print
        return

    def sombrero_test(self, precision=8):
        print('Testing 1-D DVR with a sombrero potential')
        vF = VFactory()
        V = vF.sombrero(a=-5.)
        self.test_potential(V, num_eigs=5, precision=precision,
                            xmin=-5., xmax=5., ymax=5.)
        print
        return

    def woods_saxon_test(self, precision=8):
        print('Testing 1-D DVR with a Woods-Saxon potential')
        vF = VFactory()
        V = vF.woods_saxon(A=4)
        self.test_potential(V, num_eigs=5, precision=precision,
                            xmin=0., xmax=5.,
                            ymin=-50., ymax=0.)
        print
        return

    def test_all(self, precision=8):
        self.square_well_test(precision=precision)
        self.double_well_test(precision=precision)
        self.sho_test(precision=precision)
        self.morse_test(precision=precision)
        self.sombrero_test(precision=precision)
        self.woods_saxon_test(precision=precision)

class SincDVR(DVR):
    r"""Sinc function basis for non-periodic functions over an interval
    `x0 +- L/2` with `npts` points.
    Usage:
        d = sincDVR1D(npts, L, [x0])

    @param[in] npts number of points
    @param[in] L size of interval
    @param[in] x0 origin offset (default=0)
    @attribute a step size
    @attribute n vector of x-domain indices
    @attribute x discretized x-domain
    @attribute k_max cutoff frequency
    @method h return hamiltonian matrix
    @method f return DVR basis vectors
    """
    def __init__(self, npts, L, x0=0.):
        # L = float(L)
        self.npts = npts
        self.L = L
        self.x0 = x0
        self.a = L / npts
        self.n = np.arange(npts)
        self.x = self.x0 + self.n * self.a - self.L / 2. + self.a / 2.
        self.w = np.ones(npts, dtype=np.float64) * self.a
        self.k_max = np.pi/self.a

    def t(self, hc=1., mc2=1.):
        """Return the kinetic energy matrix.
        Usage:
            T = self.t()

        @returns T kinetic energy matrix
        """
        _m = self.n[:, np.newaxis]
        _n = self.n[np.newaxis, :]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            T = 2. * (-1.)**(_m-_n) / (_m-_n)**2. / self.a**2.
        T[self.n, self.n] = np.pi**2. / 3. / self.a**2.
        T *= 0.5 * hc**2. / mc2   # (pc)^2 / (2 mc^2)
        return T

    def ip(self, hbar=1.):
        """Return the momentum matrix times i (imaginary number)
        i.e. ip = hbar frac{d}{dx}
        Usage:
            iP = self.p()

        @returns iP momentum matrix times i (imaginary number)
        """
        _m = self.n[:, np.newaxis]
        _n = self.n[np.newaxis, :]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            iP = (-1.)**(_m-_n) / (_m-_n) / self.a
        iP[self.n, self.n] = 0.
        iP *= hbar
        return iP

    def f(self, x=None):
        """Return the DVR basis vectors"""
        if x is None:
            x_m = self.x[:, np.newaxis]
        else:
            x_m = np.asarray(x)[:, np.newaxis]
        x_n = self.x[np.newaxis, :]
        return np.sinc((x_m-x_n)/self.a)/np.sqrt(self.a)

class SincDVRPeriodic(SincDVR):
    r"""Sinc function basis for periodic functions over an interval
    `x0 +- L/2` with `N` points."""
    def __init__(self, *v, **kw):
        # Small shift here for consistent abscissa
        SincDVR.__init__(self, *v, **kw)
        self.x -= self.a/2.

    def t(self, hc=1., mc2=1.):
        """Return the kinetic energy matrix.
        Usage:
            T = self.t(V)

        @returns T kinetic energy matrix
        """
        _m = self.n[:, np.newaxis]
        _n = self.n[np.newaxis, :]
        _arg = np.pi*(_m-_n)/self.npts
        if (0 == self.npts % 2):
            T = 2.*(-1.)**(_m-_n)/np.sin(_arg)**2.
            T[self.n, self.n] = (self.npts**2. + 2.)/3.
        else:
            T = 2.*(-1.)**(_m-_n)*np.cos(_arg)/np.sin(_arg)**2.
            T[self.n, self.n] = (self.npts**2. - 1.)/3.
        T *= (np.pi/self.L)**2.
        T *= 0.5 * hc**2. / mc2   # (pc)^2 / (2 mc^2)
        return T

    def f(self, x=None):
        """Return the DVR basis vectors"""
        if x is None:
            x_m = self.x[:, np.newaxis]
        else:
            x_m = np.asarray(x)[:, np.newaxis]
        x_n = self.x[np.newaxis, :]
        f = np.sinc((x_m-x_n)/self.a)/np.sinc((x_m-x_n)/self.L)/np.sqrt(self.a)
        if (0 == self.npts % 2):
            f *= np.exp(-1j*np.pi*(x_m-x_n)/self.L)
        return f

class SineDVR(DVR):
    r"""Sine function basis for non-periodic functions over an interval
    `x_min ... x_max` with `npts` points.
    Usage:
        d = sincDVR1D(npts, xmin, xmax)

    @param[in] npts number of points
    @param[in] xmin "left" end of interval
    @param[in] xmax "right" end of interval
    @attribute a step size
    @attribute n vector of x-domain indices
    @attribute x discretized x-domain
    @attribute k_max cutoff frequency
    @attribute L size of x-domain
    @method h return hamiltonian matrix
    @method f return DVR basis vectors
    """
    def __init__(self, npts, xmin=-1., xmax=1.):
        self.npts = npts
        self.L = float(xmax) - float(xmin)
        self.a = self.L / float(npts + 1.)
        self.n = np.arange(1, npts + 1)
        self.x = float(xmin) + self.a * self.n
        self.k_max = None

    def t(self, hc=1., mc2=1.):
        """Return the kinetic energy matrix.
        Usage:
            T = self.t(V)

        @returns T kinetic energy matrix
        """
        _i = self.n[:, np.newaxis]
        _j = self.n[np.newaxis, :]
        m = self.npts + 1
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            T = ((-1.)**(_i-_j)
                * (1./np.square(np.sin(np.pi / (2. * m) * (_i-_j)))
                - 1./np.square(np.sin(np.pi / (2. * m) * (_i+_j)))))
        T[self.n - 1, self.n - 1] = 0.
        T += np.diag((2. * m**2. + 1.) / 3.
                     - 1./np.square(np.sin(np.pi * self.n / m)))
        T *= np.pi**2. / 2. / self.L**2. #prefactor common to all of T
        T *= 0.5 * hc**2. / mc2   # (pc)^2 / (2 mc^2)
        return T

#     def f(self, x=None):
#         """Return the DVR basis vectors"""
#         if x is None:
#             x_m = self.x[:, np.newaxis]
#         else:
#             x_m = np.asarray(x)[:, np.newaxis]
#         x_n = self.x[np.newaxis, :]
#         return np.sinc((x_m-x_n)/self.a)/np.sqrt(self.a)

class HermiteDVR(DVR):
    r"""Hermite function basis for non-periodic functions over an interval
    `-x_max ... x_max` with `npts` points.
    Usage:
        d = sincDVR1D(npts, xmax, [x0])

    @param[in] npts number of points
    @param[in] xmax "right" end of interval
    @param[in] x0 shifted center of interval
    @attribute a step size
    @attribute n vector of x-domain indices
    @attribute x discretized x-domain
    @attribute w quadrature weights
    @attribute k_max cutoff frequency
    @attribute L size of x-domain
    @method h return hamiltonian matrix
    @method f return DVR basis vectors
    """
    def __init__(self, npts, xmax=None, x0=0.):
        assert (npts < 269), \
            "Must make npts < 269 for numpy to find quadrature points."
        self.npts = npts
        self.x0 = float(x0)
        self.n = np.arange(npts)
        c = np.zeros(npts+1)
        c[-1] = 1.
        self.x = np.polynomial.hermite.hermroots(c)
        if xmax is None:
            self.gamma = 1.
        else:
            assert xmax is None, "Sorry, xmax is currently broken"
            self.gamma = self.x.max() / float(xmax)

        self.x = self.x0 + self.x / self.gamma
        self.w = np.exp(-np.square(self.x))
        self.L = self.x.max() - self.x.min()
        self.a = None
        self.k_max = None

    def t(self, hc=1., mc2=1.):
        """Return the kinetic energy matrix.
        Usage:
            T = self.t(V)

        @returns T kinetic energy matrix
        """
        _i = self.n[:, np.newaxis]
        _j = self.n[np.newaxis, :]
        _xi = self.x[:, np.newaxis]
        _xj = self.x[np.newaxis, :]

        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            T = 2.*(-1.)**(_i-_j)/(_xi-_xj)**2.

        T[self.n, self.n] = 0.
        T += np.diag((2. * self.npts + 1.
                      - np.square(self.x)) / 3.)
        T *= self.gamma
        T *= 0.5 * hc**2. / mc2   # (pc)^2 / (2 mc^2)
        return T

#     def f(self, x=None):
#         """Return the DVR basis vectors"""
#         if x is None:
#             x_m = self.x[:, np.newaxis]
#         else:
#             x_m = np.asarray(x)[:, np.newaxis]
#         x_n = self.x[np.newaxis, :]
#         return np.sinc((x_m-x_n)/self.a)/np.sqrt(self.a)

class BesselDVR(DVR):
    r"""Bessel function basis for non-periodic functions over an interval
    `0 ... R` with `npts` points, `dim` dimensions, `lam` angular
    momentum number.
    Usage:
        d = sincDVR1D(npts, R, dim, lam)

    @param[in] npts number of points
    @param[in] R max radius
    @param[in] dim dimension of the Bessel representation
    @param[in] lam angular momentum quantum number
    @attribute n vector of domain indices
    @attribute z discretized domain
    @attribute nu
    @attribute K
    @attribute r
    @method h return hamiltonian matrix
    @method f return DVR basis vectors
    """
    def __init__(self, npts, R, dim, lam):
        assert type(dim) == int, "dim must be an integer."
        assert dim > 1, "dim must be 2 or more."
        self.npts = npts
        self.n = np.arange(npts)
        self.R = R
        self.dim = dim
        self.lam = lam
        self.__init_private()

    def __init_private(self):
        self.nu = self.lam + self.dim/2. - 1.
        self.z = bessel.j_root(nu=self.nu, N=self.npts)
        self.K = self.z[-1] / self.R
        self.x = self.z / self. K

    def t(self, hc=1., mc2=1.):
        """Return the kinetic energy matrix.
        Usage:
            T = self.t(V)

        @returns T kinetic energy matrix
        """
        n = self.npts
        nu = self.nu
        K = self.K

        _i = self.n[:, np.newaxis]
        _j = self.n[np.newaxis, :]
        _xi = self.z[:, np.newaxis]
        _xj = self.z[np.newaxis, :]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            T = 8. * K**2. * (-1.)**(_i-_j) * _xi * _xj /(_xi**2. - _xj**2.)**2.
        T[self.n, self.n] = 0.
        T += np.diag(K**2. / 3. * (1. + 2. * (nu**2. - 1.) / self.z**2.))
        T *= 0.5 * hc**2. / mc2   # (pc)^2 / (2 mc^2)
        return T

class GaussianDVR(DVR):
    pass

class ChebDVR(DVR):
    pass

# Factory functions to build different potentials:
# A factory is a function that makes a function.
class VFactory(object):
    """Factory functions to build different potentials
    A factory is a function that returns other functions.
    """
    def square_well(self, depth = 1., width = 1.,
                    origin = 0., o_val = 0.):
        """Usage:
                V = square_well_factory(**kwargs)

        Returns a function of a single variable V(x),
        representing the square-well potential:

             (-A/2, V0)            (A/2, V0)
        ------------       +       ----------------
                   |               |
                   |               |
                   |               |
                   |               |
         (-A/2, 0) |-------+-------| (A/2, 0)
                         (0, 0)

        Keyword arguments:
        @param[in] depth    Depth of the potential well (default=1)
        @param[in] width    Width of the potential well (default=1)
        @param[in] origin   Location of the well's center (default=0)
        @param[in] o_val    Value of the potential at origin (default=0)
        @returns   V        The square well potential function V(x)
        """
        def V(x):
            interior_idx = np.abs(x - origin) < width / 2.
            V = np.ones_like(x) * (depth + o_val)
            V[interior_idx] = o_val
            return V
        return V

    def double_well(self, x1 = -2., x2 = -1., x3 = 1.,
                    x4 = 2., V1 = 1., V2 = 0.,
                    V3 = 1., V4 = 0., V5 = 1.):
        """Usage:
                V = double_square_well_factory(**kwargs)

        Returns a one-dimensional potential function that represents
        a double-square-well potential. The potential looks like

           (x1, V1)      (x2, V3)   (x3, V3)      (x4, V5)
        ----------            ---------            ----------
                 |            |       |            |
                 |            |       |            |
                 |            |       |            |
                 |            |       |            |
                 |____________|       |____________|
           (x1, V2)      (x2, V2)   (x3, V4)      (x4, V4)

        Keywork arguments
        @param[in] x1    x-coordinate x1 above (default=-2)
        @param[in] x2    x-coordinate x2 above (default=-1)
        @param[in] x3    x-coordinate x3 above (default=1)
        @param[in] x4    x-coordinate x4 above (default=2)
        @param[in] V1    constant V1 above (default=1)
        @param[in] V2    constant V2 above (default=0)
        @param[in] V3    constant V3 above (default=1)
        @param[in] V4    constant V4 above (default=0)
        @param[in] V5    constant V5 above (default=1)
        @returns   V     double square-well potential V(x)
        """
        assert (x1 < x2 < x3 < x4), \
            "x-coordinates do not satisfy x1 < x2 < x3 < x4"
        def V(x):
            l_well_idx = np.logical_and(x < x2, x > x1)
            r_well_idx = np.logical_and(x < x4, x > x3)
            middle_idx = np.logical_and(x >= x2, x <= x3)
            far_rt_idx = np.greater_equal(x, x4)
            V = np.ones_like(x) * V1
            V[l_well_idx] = V2
            V[middle_idx] = V3
            V[r_well_idx] = V4
            V[far_rt_idx] = V5
            return V
        return V

    def sho(self, k = 1., x0 = 0.):
        """Usage:
                V = harmosc_factory(**kwargs)

        Return a one-dimensional harmonic oscillator potential V(x)
        with wavenumber k. i.e. V(x) = 1/2 * k * (x - x0)^2

        Keyword arguments
        @param[in] k    wavenumber of the SHO potential (default=1)
        @param[in] x0   displacement from origin (default=0)
        @returns   V    1-D SHO potential V(x)
        """
        def V(x): return 0.5 * k * np.square(x - x0)
        return V

    def power(self, a = 1., p=1., x0 = 0.):
        """Usage:
                V = self.power(**kwargs)

        Return a potential V(x) = a * (x - x0)^p

        Keyword arguments
        @param[in] a    coefficient (default=1)
        @param[in] p    power to raise x (default=1)
        @param[in] x0   displacement from origin (default=0)
        @returns   V    1-D cubic potential V(x)
        """
        def V(x): return a * np.power(x - x0, p)
        return V

    def morse(self, D = 1., a = 1., x0 = 0.):
        """Usage:
                V = morse_factory(**kwargs)

        Return a one-dimensional Morse potential V(x)
        i.e. V(x) = D * (1 - exp(-a * (x - x0)))^2 - D

        Keyword arguments
        @param[in] D    dissociation depth
        @param[in] a    inverse "width" of the potential
        @param[in] x0   equilibrium bond distance
        @returns   V    Morse potential V(x)
        """
        def V(x):
            return D * np.power(1. - np.exp(-a * (x - x0)), 2.) - D
        return V

    def sombrero(self, a = -10., b = 1.):
        """Usage:
                V = sombrero_factory(**kwargs)

        Return a one-dimensional version of the sombrero potential
        i.e. V(x) = a * x^2 + b * x^4
        This function asserts a < 0 and b > 0

        Keyword arguments
        @param[in] a    coefficient of the x^2 term (default=-10)
        @param[in] b    coefficient of the x^4 term (default=1)
        @returns   V    1-D Mexican hat potential V(x)
        """
        assert (a < 0), "Coefficient a must be negative"
        assert (b > 0), "Coefficient b must be positive"
        def V(x):
            return a * np.square(x) + b * np.power(x, 4)
        return V

    def woods_saxon(self, V0 = 50., z = 0.5, r0 = 1.2, A = 16):
        """Usage:
                V = woods_saxon_factory(**kwargs)

        Return a Woods-Saxon potential
        i.e. V(r) = - V0 / (1. + exp((r - R) / z))
        where R = r0 * A^(1/3)

        Keyword arguments
        @param[in] V0   potential depth (default=50.)
        @param[in] z    surface thickness (default=0.5)
        @param[in] r0   rms nuclear radius (default=1.2)
        @param[in] A    mass number (default=16)
        @returns   V    Woods-Saxon potential V(r)
        """
        def V(r):
            x0 = r0 * np.power(A, 1. / 3.)
            return -V0 / (1. + np.exp((r - x0)/ z))
        return V

if __name__ == '__main__':
    dvr = SincDVR(npts=200, L=14)
    x = dvr.x

    def v(x):
        return x**2

    dvr.run(v, num_eigs=8)

    # dvr = HermiteDVR(npts=20)


from appium import webdriver

"""
02：
使用Appium Desktop录制的脚本：
使用小米系统拨号，拨打10086
博客：https://blog.csdn.net/tt75281920/article/details/115646935?spm=1001.2014.3001.5501
"""

caps = {}
caps["platformName"] = "Android"
caps["deviceName"] = "d0a49989"
caps["appPackage"] = "com.android.contacts"
caps["appActivity"] = "com.android.contacts.activities.TwelveKeyDialer"
caps["noReset"] = True

driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)
driver.implicitly_wait(10)

el1 = driver.find_element_by_accessibility_id("一")
el1.click()
el2 = driver.find_element_by_accessibility_id("零")
el2.click()
el2.click()
el3 = driver.find_element_by_accessibility_id("八")
el3.click()
el4 = driver.find_element_by_accessibility_id("六")
el4.click()
el5 = driver.find_element_by_accessibility_id("拨打电话")
el5.click()

driver.quit()


from secrets import token_urlsafe
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import RedirectResponse
from pydantic import AnyUrl

from db import DB

api_router = APIRouter()

@api_router.post("/link")
def new_link(long_link: AnyUrl):
    db = DB.get_instance()
    length = 50
    while True:
        new_short_link = token_urlsafe(int(length/10))
        if db.get_link(new_short_link) is not None:
            length += 1
            continue
        db.add_link(long_link, new_short_link)
        break
    return {"short_link": new_short_link}

@api_router.get("/r/{short_link}")
def get_link(short_link: str):
    db = DB.get_instance()
    long_link = db.get_link(short_link)
    if long_link is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Can't find long link")
    return RedirectResponse(url=long_link)


"""
-------------------------------------------------------------------------------
Created on April,27, 2020
author: Suksmono@{STEI-ITB, MDR Inc.}
Find a Hadamard matrix using QA Computer/DWave
\-------------------------------------------------------------------------------
"""
# from sympy import *
# #from prb_symbolic import *
# from prb_williamson import *
import neal
import numpy as np
###
import pandas as pd
import matplotlib.pyplot as plt
##
# from prb_small_williamson import *
from prb_turyn import *
#
def writeHMatrix(fname, matx):
   # df=pd.DataFrame(data=matx.astype(int))
    df=pd.DataFrame(data=matx)
    df.to_csv(fname,sep=',', header=False, index=False)

def readHMatrix(fname):
    data=pd.read_csv(fname,sep=',', header=None )
    # return data.as_matrix() #.tolist()
    return data.values #.tolist()
#
def construct_XYZW_normal(v):
    # corr are taken from ABCD, not XYZW
    Nr=len(v)
    N=int((Nr+11)/4)
#    print('N=',N)
    #-- init XYZW
    X=np.zeros([N,1])
    Y=np.zeros([N,1])
    Z=np.zeros([N,1])
    W=np.zeros([N-1,1])
    #
    stX=0
    edX=N-4
    #
    stY=edX #+1
    edY=2*N-7
    stZ=edY#+1
    edZ=3*N-9
    stW=edZ#+1
    edW=4*N-1-10
#    print('stX:',stX,'stY:',stY,'stZ:',stZ,'stW:',stW)
#    print('edX:',edX,'edY:',edY,'edZ:',edZ,'edW:',edW)
    #
    if (edX>0):
        tX=v[stX: edX] # ;%get ss0(1) of  %X=[1; 1; 1; -1];
    else:
        tX=[]
    #--
    # --
    tY=v[stY:edY] #; %;  % get ss0(1) of Y=[1; ss0(0); -ss0(0); -1];
    tZ=v[stZ:edZ] #; %; % Z=[1; ss0(1); ss0(2); 1];
    tW=v[stW:edW] #; %;      %W =[1; ss0(3); ss0(4)];
    #
    if edX>0:
        NX=len(tX)
        for m in range(NX):
            X[m+2]=tX[m] #; !!!!!!!!
    else:
        tX=[]
    #--
    NY=len(tY)
    for m in range(NY):
        Y[m+1]=tY[m]
    #
    Y[N-2]= -Y[1] #enforcing constraint to Y
#    %

    NZ=len(tZ);
    for m in range(NZ):
        Z[m+1]=tZ[m]
#    #
    NW=len(tW)
    for m in range(NW):
        W[m+1]=tW[m]
    #
#    %---
    X[0]= 1
    Y[0]= 1
    Z[0]= 1
    W[0]= 1
    #
    X[N-1]= -1
    Y[N-1]= -1
    Z[N-1]= 1
    #
    X[1] = 1
    X[N-2]=1
    #
    return(X,Y,Z,W)
###
def contruct_XXYY(vSol, X,Y,N_XTD):
    N_ORG=len(X)
    sSol=vSol[0:NQ0]
    N_ORG05=int(N_ORG/2)
    # X->XX
    XX=np.zeros(N_ORG+N_XTD)
    XX[0:N_ORG05]=X[0:N_ORG05] #[x0,x1,*...]
    XX[N_ORG05:N_ORG05+N_XTD]=sSol[0:N_XTD]
    XX[N_ORG05+N_XTD:N_ORG+N_XTD]=X[N_ORG05:N_ORG]
    # Y->YY
    YY=np.zeros(N_ORG+N_XTD)
    YY[0:N_ORG05]=Y[0:N_ORG05] #[x0,x1,*...]
    YY[N_ORG05:N_ORG05+N_XTD]=sSol[N_XTD:2*N_XTD]
    YY[N_ORG05+N_XTD:N_ORG+N_XTD]=X[N_ORG05:N_ORG]
    return(XX,YY)

# ---
if __name__ == '__main__':
    # order definition
    N_ORG=4
    N_XTD=8
    #--
    N=N_ORG+N_XTD # final order
    #    P=N-1
    M=4*(3*N-1) # order of H-matrix
    #
    #    Nr=4*N-11 # length of random vector
    #
    NQ0=2*N_XTD
    idx=0
    ## 4->8: 4,7,18 corret
    idx_f=3 # idx file to read (+1 from the displaye idx in step-6)

    # --
    # read an entry of filled zw and incomplete xy
    path_name0='..\\py_src\\DATA\\'
    zzww_name= 'XYZW_star_xy_zzww_fill_'+str(N_ORG)+'_'+str(N_ORG+N_XTD)+ '.txt'
    fname_zzww=path_name0+zzww_name
    #
    in_file = open(fname_zzww, "r")
    #num_lines = sum(1 for line in open(fname_zzww))
    # read
    for m in range(idx_f):
        t_hex=in_file.readline()
#        print(t_hex)
        m+=1
    #--
    in_file.close()
    # convert into spin
    NHO=int(np.ceil(N_ORG/4)) #hex length of  originalspin
    NHX=int(np.ceil((N_XTD+N_ORG)/4)) #hex length of  extended spin
    #
    hX=t_hex[0:NHO]
    dX=int(hX,16)
    X=int_to_spin(dX,N_ORG)
    #
    hY=t_hex[NHO:NHO+1]
    dY=int(hY,16)
    Y=int_to_spin(dY,N_ORG)
    #
    hZZ=t_hex[2*NHO:2*NHO+NHX]
    dZZ=int(hZZ,16)
    ZZ=int_to_spin(dZZ,N_ORG+N_XTD)
    #
    hWW=t_hex[2*NHO+NHX:2*NHO+2*NHX]
    dWW=int(hWW,16)
    WW0=int_to_spin(dWW,N_ORG+N_XTD)
    WW=WW0[1:N_ORG+N_XTD]

    # ------------------------------------------------------------
    #    number of required qubits
    # ------------------------------------------------------------

    print('Finding Turyn Goethals-Siedel matrix of order:', M)
    print('Reading Ising coefficients from file ...')
    #    hi_complete_XY_4_2_1
    path_name='..\\Jij_DATA\\'
    Jij_name=path_name+'Jij_complete_XY_'+str(N_ORG)+\
    '_'+str(N_XTD)+ '_'+str(idx_f)+ '.txt'
    hi_name= path_name+'hi_complete_XY_'+str(N_ORG)+'_'+str(N_XTD)+ '_'+str(idx_f)+ '.txt'
    b_name= path_name +'b_complete_XY_'+str(N_ORG)+'_'+str(N_XTD)+ '_'+str(idx_f)+ '.txt'
    #
    Jij=readHMatrix(Jij_name)
    hiA=readHMatrix(hi_name)
    hi=hiA[:,0]
    bA=readHMatrix(b_name)
    b=bA[0][0]

    #normalize
    maxCoeff=np.max([np.max(abs(hi)), np.max(abs(Jij)),b])
    hi=hi/maxCoeff
    Jij=Jij/maxCoeff
    # measure of energy minimum
    b=b/maxCoeff

    '''
    # -----------------------------------------------------------------------------
    #     Convert Ising coefficients into data of Neal's format
    # -----------------------------------------------------------------------------
    '''
    #in dictionary format
    h={0:0}
    J={(0,1):1}

    for m in range(0,len(hi)):
        h[m]=hi[m]
        for n in range (m+1,len(hi)):
            J[m,n]=Jij[m,n]
        # --
    # --- end of filling h and J

    '''
    -----------------------------------------------------------------------------
    4. SOLVE THE PROBLEM
    -----------------------------------------------------------------------------
    '''
    #
    print('Solving the problem using Neal  ...')
    solver=neal.SimulatedAnnealingSampler()
    NSWEEPS=1*M*M #1*1*10*1000 #1000*1000
    NR=10*1000 #*1000 #*10 #25 #100
    response = solver.sample_ising(h, J, sweeps=NSWEEPS, num_reads=NR)
    #
    vE=response.data_vectors['energy']
    aSol=response.record['sample']
    #
    print('Configurations:\n', aSol)
    print('Energy:\n',vE)
    #
    idxMinE=np.argmin(vE)
    print('Minimum Energy:',vE[idxMinE])#, 'supposed to be', -b)
    print('Minimum Configurations:',aSol[idxMinE])
    tSol=aSol[idxMinE]
    vSol=tSol#[0]
    #
    XX,YY=contruct_XXYY(vSol, X,Y,N_XTD)
    H=construct_hadamard(XX,YY,ZZ,WW)
    DG=np.matmul(H, H.transpose())
    # display indicator
    plt.imshow(DG.astype(float), cmap="gray")
    plt.show()
    # display H-matrix
    plt.imshow(H.astype(float), cmap="gray")
    plt.show()
    if is_hadamard(H):
        print('Hadamard matrix is found ...!!!')
    else:
        print('Hadamard matrix is NOT found in minimum Energy...!!!')
    ## check all solutions
    NR,NC=aSol.shape
    idx=0
    vIdx=[]
    for m in range(NR):
        tt=aSol[m,:]
        vtt=tt#[0]
        #--
        XX,YY=contruct_XXYY(tt[0:NQ0], X,Y,N_XTD)
        H=construct_hadamard(XX,YY,ZZ,WW)
        if is_hadamard(H):
            vIdx.append(m)
            idx=idx+1
    #--
    if(len(vIdx)>0):
        print('Solutions found: ',len(vIdx))
        tSol=aSol[vIdx[0],:]
        vSol=tSol#[0]
        #--
        XX,YY=contruct_XXYY(vSol[0:NQ0], X,Y,N_XTD)
        H=construct_hadamard(XX,YY,ZZ,WW)
        DG=np.matmul(H, H.transpose())
        plt.imshow(DG.astype(float), cmap="gray")
        plt.show()


import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
import numpy as np
from pathlib import Path
from IPython.core.pylabtools import figsize
import matplotlib.pyplot as plt

from net import GlobalCoarseNet, LocalFineNet
from loss import ScaleInvariantLoss
import util

# ignore stupid warning
import warnings
warnings.filterwarnings('ignore')

# training device
device = torch.device('cuda')

# hyperparameter
epochs_num = 4
rate = 1 # rate = 5 in paper; but result is bad so it is tuned by hand

# specifed in paper
batch_size = 32

# load dataset
dataloader_train, dataloader_valid, train_num, val_num = util.load_train(Path('nyu/train'), Path('nyu/val'), batch_size)

print("training number:{}, validation number:{}".format(train_num, val_num))

#########################################################
#              initializing the model                   #
#########################################################

# initialize and parallize the models
global_net = GlobalCoarseNet().to(device)
local_net = LocalFineNet().to(device)

# loss
global_loss = ScaleInvariantLoss()
local_loss = ScaleInvariantLoss()

# optimizer
global_optimizer = torch.optim.SGD([{'params': global_net.coarse6.parameters(), 'lr': 0.1*rate},
                                    {'params': global_net.coarse7.parameters(), 'lr': 0.1*rate}],
                                    lr = 0.001*rate, momentum = 0.9, weight_decay = 0.1)

local_optimizer = torch.optim.SGD([{'params': local_net.fine2.parameters(), 'lr': 0.01*rate}],
                                    lr = 0.001*rate, momentum = 0.9, weight_decay = 0.1)

# parallel
global_net = nn.DataParallel(global_net)
local_net = nn.DataParallel(local_net)

##########################################################
#       training global coarse net with validation       #
##########################################################

print("=============global coarse net training===============")

train_losses = []
val_losses = []

for e in range(epochs_num):
    # train
    train_loss = 0
    global_net.train()
    for i, sample in enumerate(dataloader_train):
        rgb = sample[0].float().to(device)
        depth = sample[1].float().to(device)

        # forward pass
        output = global_net(rgb)
        loss = global_loss(output, depth)

        # back propagate
        global_net.zero_grad()
        loss.backward()

        # optimization
        global_optimizer.step()
        train_loss += loss.item()

    train_losses.append(train_loss / train_num)

    # validation - evaluation mode
    val_loss = 0
    global_net.eval()
    with torch.no_grad():
        for i, sample in enumerate(dataloader_valid):
            rgb = sample[0].float().to(device)
            depth = sample[1].float().to(device)

            # forward pass
            output = global_net(rgb)
            loss = global_loss(output, depth)
            val_loss += loss.item()

    val_losses.append(val_loss / val_num)

    # save model
    torch.save(global_net, './models/global_net.pt')

    print(('epoch {}: train_loss = {}, validation loss = {}').format(e, train_loss, val_loss))

########################################################
#        training local fine net with validation       #
########################################################

print("=============local fine net training===============")

train_losses_ = []
val_losses_ = []

for e in range(epochs_num):
    # train
    train_loss = 0
    local_net.train()
    for i, sample in enumerate(dataloader_train):
        rgb = sample[0].float().to(device)
        depth = sample[1].float().to(device)

        # results from global coarse network
        global_net.eval()
        with torch.no_grad():
            global_output = global_net(rgb).unsqueeze(1)

        # forward pass
        output = local_net(rgb, global_output).squeeze(1)
        loss = local_loss(output, depth)

        # back propagate
        local_net.zero_grad()
        loss.backward()

        # optimization
        local_optimizer.step()
        train_loss += loss.item()

    train_losses_.append(train_loss / train_num)

    # valid
    val_loss = 0
    local_net.eval()
    with torch.no_grad():
        for i, sample in enumerate(dataloader_valid):
            rgb = sample[0].float().to(device)
            depth = sample[1].float().to(device)

            # results from global coarse network
            global_net.eval()
            with torch.no_grad():
                global_output = global_net(rgb).unsqueeze(1)

            # forward pass
            output = local_net(rgb, global_output).squeeze(1)
            loss = local_loss(output, depth)

            val_loss += loss.item()
    val_losses_.append(val_loss / val_num)

    # save model
    torch.save(local_net, './models/local_net.pt')

    print(('epoch {}: train_loss = {}, validation loss = {}').format(e, train_loss, val_loss))

######################################################
#      visulizing the training result on val set     #
######################################################

util.plot_loss(train_losses, val_losses)
util.plot_loss(train_losses_, val_losses_)

print("==========result visualized on validation set=============")
rgb = list(dataloader_valid)[0][0].float().to(device)
depth = list(dataloader_valid)[0][1].float().to(device)

# results from global coarse network
global_net.eval()
with torch.no_grad():
    global_output = global_net(rgb).unsqueeze(1)

# results from local fine network
local_net.eval()
with torch.no_grad():
    local_output = local_net(rgb, global_output)

for i in range(5):
    depth_g = global_output[i].view(74, 55)
    depth_l = local_output[i].view(74, 55)

    r = transforms.ToPILImage()((rgb[i]).cpu())
    gt = depth[i]

    figsize(12,4)
    plt.subplot(141); plt.title('RGB input'); plt.imshow(r)
    plt.subplot(142); plt.title('ground truth'); plt.imshow(gt.cpu())
    plt.subplot(143); plt.title('global coarse result'); plt.imshow(torch.exp(depth_g).cpu())
    plt.subplot(144); plt.title('local fine result'); plt.imshow(torch.exp(depth_l).cpu())

    plt.show()