text:
# -*- coding: utf-8 -*-

# -*- coding: utf-8 -*-
"""
Simulate elections.

Elements of an election

1. Create voter preferences

 - Create voter preference distributions
 - Create voter preference tolerance distribution

2. Create candidate preferences
3. Simulate voter behavior, strategy
4. Transform voter preferences into candidate scores or rankings
5. Input scores/ranks into election system.
6. Run the election.
7. Measure the results.

Object Data Transfer Model
--------------------------

Voters --> VoterGroup

Voters/VoterGroup --> Candidates

Voters, VoterGroup, Candidates --> Election

To construct models or benchmarks, start by creating object `Voters`.
`Voters` may have various properties such as preference,
voter strategy parameters, tolerance circles, etc. Define these
properties in Voters. Voters can be segregated by groups,
and each group may have different properties. `VoterGroup` is used to
define groups of several `Voters`.

After defining voters, candidates may be defined using class
`Candidate`. `Candidate` definition may be dependent on the voters population,
therefore `Candidate` accepts voters as an argument.

With the voters and candidates define, an election can be generated with
`Election`. `Election` has many subclasses which run the election.

- `BallotGenerator` takes voter and candidate information to generate honest
  and tactical ballots.
- `eRunner` handles the running of specific types of elections.
- `ElectionResult` handles the storage of output data.

"""
import collections
import pickle
import copy
from typing import List, NamedTuple

import numpy as np
import pandas as pd
import scipy
from scipy.stats import truncnorm

from votesim import metrics
from votesim import ballot
from votesim import votemethods
from votesim import utilities
from votesim.models import vcalcs
from votesim.models.dataclasses import (VoterData,
                                        VoterGroupData,
                                        CandidateData,
                                        ElectionData,
                                        ElectionResult,
                                        strategy_data,
                                        StrategyData,
                                        )
from votesim.strategy import TacticalBallots
# from votesim.strategy import TacticalBallots, FrontRunners

__all__ = [
    'Voters',
    'VoterGroup',
    'Candidates',
    'Election'
           ]

# Base random seeds
VOTERS_BASE_SEED = 2
CLIMIT_BASE_SEED = 3
CANDIDATES_BASE_SEED = 4
ELECTION_BASE_SEED = 5

#import seaborn as sns

import logging
logger = logging.getLogger(__name__)

def ltruncnorm(loc, scale, size, random_state=None):
    """
    Truncated normal random numbers, cut off at locations less than 0.

    Parameters
    -----------
    loc : float
        Center coordinate of gaussian distribution
    scale : float
        Std deviation scale
    size : int
        Number of random numbers to generate
    random_state : None or numpy.random.RandomState
        Random number seeding object, or None.

    Returns
    ---------
    out : array shaped (size)
        Output samples
    """
    if scale == 0:
        return np.ones(size) * loc

    xmin = -loc / scale
    t = truncnorm(xmin, 1e6)
    s = t.rvs(size=size, random_state=random_state)
    s = s * scale  + loc
    return s

def _RandomState(seed, level=1):
    """
    Create random state.
    Generate multiple random statse from a single seed, by specifying
    different levels for different parts of Election.

    Parameters
    ----------
    seed : int
        Integer seed
    level : int
        Anoter integer seed.
    """
    if seed is None:
        return np.random.RandomState()
    else:
        return np.random.RandomState((seed, level))

class Voters(object):
    """Create simple normal distribution of voters.

    Parameters
    ----------
    seed : int or None
        Integer seed for pseudo-random generation. None for random numbers.
    strategy : dict
        Voter regret-to-ratings conversion strategy.

    stol : float (default 1.0)
        Tolerance factor for strategy

    Features
    --------
    Score & ratings are constructed based on candidate coordinates

    Attributes
    ----------
    pref : array shape (a, b)
        Voter preferences, `a` number of voters, `b` number of preference dimensions

    strategy : dict
        Container for strategy options with keys

        tol : float
            Voter preference tolerance
        base : str
            Base honest ballot type
        tactics : list of str
            Tactic methods to apply onto ballot.
            See `votesim.ballot.TacticalBallots` for available tactics.
        onesided : bool
            Use onesided ballot, or use full strategic ballot.
        iteration : int
            Numbers of iterations of strategy to undergo.
    """

    data: VoterData

    def __init__(self, seed=None, tol=None, base='linear', order=1):
        self.init(seed, order=order)
        # if strategy is None:
        #     strategy = {}

        self.set_strategy(tol=tol, base=base)

        return

    @utilities.recorder.record_actions(replace=True)
    def init(self, seed, order: int):
        """Set pseudorandom seed & distance calculation order."""
        self.seed = seed
        self._randomstate = _RandomState(seed, VOTERS_BASE_SEED)
        self._order = order
        self._weights = None
        return self

    @utilities.recorder.record_actions(replace=True)
    def set_strategy(self,
                      tol=None,
                      base='linear',
                      # iterations=1,
                      # tactics: List[str]=(),
                      # subset='',
                      # ratio=1.0,
                      # frontrunnertype='tally',
                      # frontrunnernum=2,
                      # frontrunnertol=0.0,
                      ):
        """Set voter strategy type."""
        self._tol = tol
        self._base = base
        # strategy = {}
        # strategy['tol'] = tol
        # strategy['base'] = base
        # strategy['tactics'] = tactics
        # strategy['subset'] = subset
        # strategy['ratio'] = ratio
        # strategy['frontrunnertype'] = frontrunnertype
        # strategy['frontrunnernum'] = frontrunnernum
        # strategy['frontrunnertol'] = frontrunnertol

        # if len(tactics) == 0:
        #     iterations = 0

        # strategy['iterations'] = iterations
        # self._strategy = strategy
        return self

    @utilities.recorder.record_actions()
    def add_random(self, numvoters, ndim=1, loc=None):
        """Add random normal distribution of voters.

        Parameters
        ----------
        numvoters : int
            Number of voters to generate
        ndim : int
            Number of preference dimensions of population
        loc : array shaped (ndim,)
            Coordinate of voter centroid
        """
        rs = self._randomstate
        center = np.zeros(ndim)

        voters = rs.normal(center, size=(numvoters, ndim))
        if loc is not None:
            voters = voters + loc

        return self._add_voters(voters)

    @utilities.recorder.record_actions()
    def add_points(self, avgnum, pnum, ndim=1):
        """Add a random point with several clone voters at that point.

        Parameters
        ----------
        avgnum : int
            Avg. Number of voters per unique point
        pnum : int
            Number of unique points
        ndim : int
            Number of dimensions
        """
        rs = self._randomstate

        center = np.zeros(ndim)

        for i in range(pnum):

            # coordinate of point
            point = rs.normal(center, size=(1, ndim))

            # number of voters at the point
            voternum = ltruncnorm(1, 1, 1) * avgnum
            voternum = int(voternum)

            voters = np.ones((voternum, ndim)) * point
            self._add_voters(voters)
        return self

    @utilities.recorder.record_actions()
    def add(self, pref):
        """Add arbitrary voters.

        Parameters
        ----------
        pref : array shape (a, b)
            Voter preferences, `a` is number of voters, `b` pref. dimensions.
        """
        return self._add_voters(pref)

    def _add_voters(self, pref):
        """Base function for adding 2d array of candidates to election."""

        try:
            pref = np.row_stack((self._pref, pref))
        except (AttributeError, ValueError):
            pref = np.atleast_2d(pref)
        self._pref = pref
        return self

    def build(self):
        """Finalize Voter, construct immutable VoterData."""
        self.data = VoterData(pref=self._pref,
                              weights=self._weights,
                              order=self._order,
                              stats=None,
                              tol=self._tol,
                              base=self._base,
                              )
        return self

    def calculate_distances(self, candidates: CandidateData):
        """Preference distances of candidates from voters for building ballots.

        Parameters
        ----------
        candidates : array shaped (a, b)
            Candidate preference data
        """
        pref = self.data.pref
        order = self.data.order
        weights = self.data.weights

        distances = vcalcs.voter_distances(voters=pref,
                                           candidates=candidates.pref,
                                           weights=weights,
                                           order=order)
        return distances

    def honest_ballots(self, candidates: CandidateData):
        """Honest ballots calculated from Candidates."""
        distances = self.calculate_distances(candidates)
        b = ballot.gen_honest_ballots(distances=distances,
                                      tol=self.data.strategy['tol'],
                                      base=self.data.strategy['base'])
        return b

class VoterGroup(object):
    """Group together multiple voter objects & interact with candidates.

    Parameters
    ----------
    voters_list : list[Voters]
        List of Voters

    Attributes
    ----------
    group : list[Voters]
        Same as voters_list

    """
    def __init__(self, voters_list: List[Voters]):
        try:
            iter(voters_list)
        except Exception:
            voters_list = [voters_list]
        self.group = voters_list
        self._build()
        return

    def _build(self):
        """Finalize VoterGroup, build immutable VoterGroupData."""
        group_datas = tuple(v.build() for v in self.group)

        orders = np.array([v.data.order for v in self.group])
        if len(orders) > 0:
            order = orders[0]
            if not np.all(orders == orders[0]):
                raise ValueError('Order of voters in group must all be same.')
        else:
            order = None

        # data = self.group[0]
        # data = data.replace(pref=self._get_pref())
        # self.data = data

        pref = self._get_pref()
        stats = metrics.VoterStats(pref=pref,
                                   weights=None,
                                   order=order)

        group_index = dict(enumerate(self.group_indices))

        data = VoterGroupData(groups=group_datas,
                              pref=pref,
                              weights=None,
                              order=order,
                              stats=stats,
                              group_index=group_index,
                              )
        self.data = data
        return self

    def build(self):
        """This is a dummy build and does nothing. VoterGroup is auto-built."""
        return self

    def _get_pref(self):
        vlist = [v.data.pref for v in self.group]
        return np.vstack(vlist)

    def __getitem__(self, key):
        return self.group[key]

    @utilities.lazy_property
    def group_indices(self):
        """Row indices to obtain child's voters for all children in the voter
        preference and ballot arrays.

        Returns
        -------
        slices : list of slice
            Slice which returns the Voter group
        """
        groups = self.group
        lengths = [len(v.data.pref) for v in groups]
        iarr = np.cumsum(lengths)
        iarr = np.append(0, iarr)
        slices = [slice(iarr[i], iarr[i+1]) for i in iarr[:-1]]
        return slices

def voter_group(vlist):
    """Group together multiple Voters."""
    if hasattr(vlist, 'group'):
        return vlist
    else:
        return VoterGroup(vlist)

class Candidates(object):
    """
    Create candidates for spatial model

    Parameters
    -----------
    voters : `Voters` or `VoterGroup`
        Voters to draw population data.

    Attributes
    ----------
    pref : array shape (a, b)
        Voter preferences, `a` number of candidates,
        `b` number of preference dimensions
    """
    data: CandidateData

    def __init__(self, voters: Voters, seed=None):
        self._method_records = utilities.recorder.RecordActionCache()

        if not hasattr(voters, '__len__'):
            voters = [voters]

        self.voters = voter_group(voters)
        self.set_seed(seed)
        return

    @utilities.recorder.record_actions()
    def set_seed(self, seed):
        """ Set pseudorandom seed """
        self._seed = (seed, CANDIDATES_BASE_SEED)
        self._randomstate = _RandomState(*self._seed)
        return self

    def _add_candidates(self, candidates):
        """Base function for adding 2d array of candidates to election"""
        candidates = np.array(candidates)
        assert candidates.ndim == 2, 'candidates array must have ndim=2'

        vdata = self.voters.data

        try:
            candidates = np.row_stack((self._pref, candidates))
        except (AttributeError, ValueError):
            candidates = np.atleast_2d(candidates)

        cdim = candidates.shape[1]
        vdim = vdata.pref.shape[1]

        condition = cdim == vdim
        s = ('dim[1] of candidates (%s) '
             'must be same as dim[1] (%s) of self.voters' % (cdim, vdim))

        assert condition, s
        self._pref = candidates
        return self

    def reset(self):
        """Reset candidates for a given Voters.
        Delete candidate preferences and records"""
        try:
            self._method_records.reset()
        except AttributeError:
            pass
        try:
            del self.data
        except AttributeError:
            pass
        return

    @utilities.recorder.record_actions()
    def add_random(self, cnum, sdev=2):
        """
        Add random candidates, uniformly distributed.

        Parameters
        ----------
        cnum : int
            Number of candidates for election
        sdev : float
            +- Width of standard deviations to set uniform candidate
            generation across voter population

        """
        rs = self._randomstate
        std = self.voters.data.stats.pref_std
        mean = self.voters.data.stats.pref_mean
        ndim = std.shape[0]

        candidates = rs.uniform(low = -sdev*std,
                                high = sdev*std,
                                size = (cnum, ndim)) + mean
        return self._add_candidates(candidates)

    @utilities.recorder.record_actions()
    def add(self, candidates):
        """Add 2d array of candidates to election, record actions

        Parameters
        ----------------
        candidates : array shape (a, n)
            Candidate preference coordinates.

            - a = number of candidates
            - n = number of preference dimensions
        """
        self._add_candidates(candidates)
        return self

    def build(self):

        voters = self.voters
        pref = self._pref
        distances = vcalcs.voter_distances(voters=voters.data.pref,
                                           candidates=pref,
                                           weights=voters.data.weights,
                                           order=voters.data.order,
                                           )
        stats = metrics.CandidateStats(pref=pref,
                                       distances=distances)
        self.data = CandidateData(pref=self._pref,
                                  distances=distances,
                                  stats=stats)
        return self

class StrategiesEmpty(object):
    _strategies = []
    data = ()

    def __init__(self):
        return

    def build(self):
        return self

    def __len__(self):
        return 0

class Strategies(object):
    """Strategy constructor."""
    def __init__(self, vgroup: VoterGroup):
        self._method_records = utilities.recorder.RecordActionCache()
        self.voters = voter_group(vgroup)
        self.vlen = len(self.voters.group)

        self._strategies = []

        return

    @utilities.recorder.record_actions()
    def add(self, strategy, index):
        """Set a strategy for a specified voter group."""
        return self._set(index, strategy)

    @utilities.recorder.record_actions()
    def fill(self, strategy):
        """Set strategy for unset groups."""
        locations = self.get_no_strategy
        for ii in locations:
            self._set(ii, strategy)
        return self

    def _set(self,
             tactics: tuple,
             subset: str,
             ratio: float,
             underdog: int,
             groupnum: int,
             frontrunnertype='eliminate',
             ):

        group_index = self.voters.group_indices[index]

        d = StrategyData(tactics=tactics,
                         subset=subset,
                         ratio=ratio,)
        self._strategies.append(d)
        return self

    def build(self):
        if len(self.get_no_strategy()) > 0:
            raise ValueError('Insufficient strategies have been defined!')
        if self.has_duplicates():
            raise ValueError('Duplicate strategy entries found.')
        self.data = tuple(self._strategies)
        return self

    def get_no_strategy(self):
        """ndarray : Groups' index locations that have no strategies set."""

        no_strat_locs = []
        for ii, index in enumerate(self.voters.group_indices):
            found = False
            for strategy in self._strategies:
                if np.all(index == strategy.index):
                    found = True

            if not found:
                no_strat_locs.append(ii)
        return np.array(no_strat_locs)

    def has_duplicates(self):
        """Make sure no duplicate group index + subset locations have been defined.
        Return True if duplicates found. False otherwise."""
        data = []
        for strategy in self._strategies:
            index = strategy.index
            subset = strategy.subset
            data.append((repr(index), subset))

        count = collections.Counter(data)

        count_values = list(count.values())
        iimax = np.argmax(count_values)

        if count_values[iimax] > 1:
            logger.warn('Duplicate strategy found at strategy #%s', iimax)
            return True
        return False

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

class BallotGenerator(object):
    """
    Generate ballots from voter and candidate data.

    Parameters
    ----------
    voters_list : list of Voter or VoterGroup
        Voters of election
    candidates : Candidates
        Candidates of election

    """
    tacticalballots : TacticalBallots
    honest_ballot_dict : dict

    def __init__(self,
                 voters_list: VoterGroup,
                 candidates: Candidates,
                 scoremax):
        self.candidates = candidates
        self.votergroup = voter_group(voters_list)
        self.scoremax = scoremax
        self._init_honest_builder()
        return

    def _init_honest_builder(self):
        """Honest ballot constructor for ratings, ranks, scores, and votes."""
        cdata = self.candidates.data
        blist = []
        for voter in self.votergroup.group:
            distances = voter.calculate_distances(cdata)
            b = ballot.gen_honest_ballots(distances=distances,
                                          tol=voter.data.tol,
                                          base=voter.data.base,
                                          maxscore=self.scoremax,)
            blist.append(b)
        self.honest_ballot_gen = ballot.CombineBallots(blist)

        bdict = {}
        bdict['rank'] = self.honest_ballot_gen.ranks
        bdict['score'] = self.honest_ballot_gen.scores
        bdict['rate'] = self.honest_ballot_gen.ratings
        bdict['vote'] = self.honest_ballot_gen.votes
        self.honest_ballot_dict = bdict
        return

    def get_honest_ballots(self, etype):
        btype = votemethods.get_ballot_type(etype)
        return self.honest_ballot_dict[btype]

    def get_ballots(self, etype, strategies=(), result=None, ballots=None):
        """Retrieve tactical ballots.

        Parameters
        ----------
        etype : str
            Election method
        strategies : list of `StrategyData`
            Voter strategies to apply onto ballots
        result : `ElectionResult`
            Previous results which can be used to calculate front runner.

        Returns
        -------
        ballots : ndarray (v, c)
            New ballots
        group_index : dict
            Index locations of voter groups.

        """
        if len(strategies) == 0:
            ballots = self.get_honest_ballots(etype)
            group_index = self.votergroup.data.group_index
        else:
            if ballots is None:
                ballots = self.get_honest_ballots(etype)
            if result is None:
                raise ValueError('A previous honest result must be provided for tactical ballots.')

            tballot_gen = TacticalBallots(etype=etype,
                                          strategies=strategies,
                                          result=result,
                                          ballots=ballots)
            ballots = tballot_gen.ballots
            group_index = tballot_gen.group_index

            # Just save this thing might be useful for debugging.
            self.tacticalballots = tballot_gen

        return ballots, group_index

    # @utilities.lazy_property
    # def is_all_honest_voters(self):
    #     """bool : Determine if all voter groups are honest."""
    #     for voter in self.group:
    #         if len(voter.data.strategy['tactics']) > 0:
    #             return False
    #     return True

class ___BallotGenerator(object):
    """
    Generate ballots from voter and candidate data.

    Parameters
    ----------
    voters_list : list of Voter or VoterGroup
        Voters of election
    candidates : Candidates
        Candidates of election

    """
    def __init__(self, voters_list: VoterGroup, candidates: Candidates):
        self.candidates = candidates
        self.group = voter_group(voters_list).group

    @utilities.lazy_property
    def honest_ballot_gen(self) -> ballot.CombineBallots:
        """Combined honest ballots for all voters in all groups."""
        logger.info('Constructing honest ballots.')
        blist = [v.honest_ballots(self.candidates.data) for v in self.group]
        new = ballot.CombineBallots(blist)
        return new

    def ballots(self,
                etype: str,
                ballots=None,
                result: "ElectionResult"=None):
        """Generate ballots according specified voter strategy.

        One-sided index information for `self.index_dict` is also constructed
        when tactical ballots are constructed.

        Parameters
        ----------
        etype : str
            Election type
        ballots : ballot subclass
            Optional, Initial ballots
        erunner : eRunner class
            Optional, Previous election runnner if available.

        Returns
        -------
        out : TacticalBallots
            Ballots used for election
        """
        #indices = self.honest_ballots.children_indices
        #maxiter = max(v.strategy['iterations'] for  v in self.group)
        if ballots is None:
            b0 = self.honest_ballots
        else:
            b0 = ballots
        if self.is_all_honest_voters():
            return b0

        logger.info('Constructing tactical ballots')
        # Retrieve initial front runners
        # frontrunners_init = b
        # erunner = b0.erunner
        self.clean_index()

        b = TacticalBallots(etype, ballots=b0, result=result)
        indices = self.index_dict_tactical

        # Set tactics for each group
        # for jj, vindex in enumerate(indices):
        for jj, (key, vindex) in enumerate(indices.items()):
            voters = self.group[jj]
            strategy = voters.data.strategy
            # iterations = strategy['iterations']
            # if ii < iterations:
            b.set(tactics=strategy['tactics'],
                  subset=strategy['subset'],
                  frontrunnernum=strategy['frontrunnernum'],
                  frontrunnertype=strategy['frontrunnertype'],
                  frontrunnertol=strategy['frontrunnertol'],

                  index=vindex
                  )

            # Record group index locations for one-sided tactics
            # if ii == iterations - 1:
            # if strategy['onesided'] == True:
            name = str(jj) + '-tactical-underdog'
            self.index_dict[name] = np.where(b.iloc_bool_underdog)[0]
            name = str(jj) + '-tactical-topdog'
            self.index_dict[name] = np.where(b.iloc_bool_topdog)[0]

        # To perform next iteration, set the base ballot to the newly
        # constructed tactical ballots
        # b0 = b
        return b

    def is_all_honest_voters(self):
        """bool : Determine if all voter groups are honest."""
        for voter in self.group:
            if len(voter.data.strategy['tactics']) > 0:
                return False
        return True

    @utilities.lazy_property
    def index_dict(self):
        """dict : Index locations of voters for each group.

        If one-sided tactical ballots are generated, index locations for
        '-topdog' and '-underdog' voters are also included."""

        d = self.index_dict_groups.copy()
        for key, value in self.index_dict_tactical.items():
            d[key + '-tactical'] = value

        for key, value in self.index_dict_honest.items():
            d[key + '-honest'] = value

        return d

    @utilities.lazy_property
    def index_dict_groups(self):
        """dict : Index locations of voters for each group.

        If one-sided tactical ballots are generated, index locations for
        '-topdog' and '-underdog' voters are also included."""
        indices = self.honest_ballots.children_indices
        index_dict = {}
        for ii, index in enumerate(indices):
            index_dict[str(ii)] = index
        #self._index_dict = index_dict
        return index_dict

    @property
    def index_dict_tactical(self):
        return self._index_dict_tactical_honest[0]

    @property
    def index_dict_honest(self):
        return self._index_dict_tactical_honest[0]

    @utilities.lazy_property
    def _index_dict_tactical_honest(self):
        """Calculate index locations of tactical voters and honest voters for
        each group."""
        dict_tactical= {}
        dict_honest = {}
        group_num = len(self.group)
        for ii in range(group_num):

            group = self.group[ii]
            slicei = self.honest_ballots.children_indices[ii]
            starti = slicei.start
            stopi = slicei.stop

            strategy = group.data.strategy
            voter_num = len(group.data.pref)

            try:
                ratio = strategy['ratio']
            except KeyError:
                # Assume 100% strategic voters if ratio not found.
                ratio = 1.0

            if len(strategy['tactics']) > 0:
                strat_voter_num = int(np.round(ratio * voter_num))
                endi = starti + strat_voter_num
                index_tactical = np.arange(starti, endi)
                index_honest = np.arange(endi, stopi)
            else:
                index_tactical = np.array([], dtype=int)
                index_honest = np.arange(starti, stopi)

            dict_tactical[str(ii)] = index_tactical
            dict_honest[str(ii)] = index_honest
        return dict_tactical, dict_honest

    def reset(self):
        utilities.clean_lazy_properties(self)

    def clean_index(self):
        names = ['index_dict',
                 '_index_dict_tactical_honest',
                 'index_dict_groups']
        utilities.clean_some_lazy_properties(self, names)

    @property
    def distances(self):
        """(a, b) array: `a` Voter preference distances from `b` candidates."""
        return self.honest_ballots.distances

    def __getitem__(self, key):
        return self.group[key]

class Election(object):
    """
    Run an Election with Voters and Candidates

    Parameters
    ------------
    voters : None, Voters, VoterGroup, or list of Voters
        Voters object specifying the voter preferences and behavior.
    candidate : None or Candidates
        Candidates object specifying candidate preferences
    seed : int or None
        Seed for pseudo-random number generation
    numwinners : int >= 1
        Number of winners for the election
    scoremax : int
        Maximum score for ballot generation
    name : str
        Name of election model, used to identify different benchmark models.
    save_args : bool (default True)

        - If True, save all parameters input into method calls. These
          parameters can be used to regenerate specific elections.
        - If False, only save parameters input into `self.user_data`.

    Attributes
    ----------
    result : ElectionResult
        Results storage for Election.
    ballotgen : BallotGenerator
        VoterBallot data

    """

    data: ElectionData
    result: ElectionResult
    strategies: Strategies
    ballotgen : BallotGenerator

    def __init__(self,
                 voters: VoterGroup=None,
                 candidates: Candidates=None,
                 strategies: Strategies=None,
                 seed=None,
                 numwinners=1,
                 scoremax=5,
                 name = '',
                 save_args=True,
                 save_records=True):

        self._method_records = utilities.recorder.RecordActionCache()

        self.voters: Voters = None
        self.candidates: Candidates = None
        self.ballotgen: BallotGenerator = None
        self.strategies: Strategies = StrategiesEmpty()

        self.save_args = save_args
        self.save_records = save_records

        self.init(seed, numwinners, scoremax, name)
        self.set_models(voters, candidates, strategies)
        self._result_calc = ElectionResultCalc(self)
        return

    @utilities.recorder.record_actions(replace=True)
    def init(self, seed, numwinners, scoremax, name):
        """Initialize some election properties"""
        self._set_seed(seed)
        self.numwinners = numwinners
        self.scoremax = scoremax
        self.name = name
        return

    def set_models(
            self,
            voters: Voters=None,
            candidates: Candidates=None,
            strategies: Strategies=None,
            ):
        """Set new voter or candidate model.

        Parameters
        ----------
        voters : Voters or None
            New voters object.
            If None, use previously inputed Voters.
        candidates : Candidates or None
            New candidates object.
            If None, use previously inputed Candidates.
        strategies : `votesim.models.spatial.Strategies`
            New strategies object.
            If None, use the previously inputed Strategies
        """

        if voters is not None:
            self.voters = voter_group(voters)

        if candidates is not None:

            self.candidates = candidates.build()
            if self.voters is not None:
                self.ballotgen = BallotGenerator(
                    self.voters,
                    self.candidates,
                    scoremax=self.scoremax
                    )
        if strategies is not None:
            if len(strategies) > 0:
                self.strategies = strategies.build()
            else:
                self.strategies = strategies
            # except AttributeError:

            # elif hasattr(strategies, 'build'):
            #     self.strategies = strategies.build().data
            # else:
            #     try:
            #         strategies[0].tactics
            #         self.strategies = strategies
            #     except (IndexError, AttributeError):
            #         s = f'{strategies}' + \
            #              ' is probably not a valid strategies input.'
            #         raise ValueError(s)

        return

    def _set_seed(self, seed):
        """ Set pseudorandom seed """
        if seed is None:
            self._seed = None
            self._randomstate = _RandomState(None)
        else:
            self._seed = (seed, ELECTION_BASE_SEED)
            self._randomstate = _RandomState(*self._seed)
        return

    def user_data(self, d=None, **kwargs):
        """Record any additional data the user wishes to record.

        Parameters
        ----------
        **d : dict
            Write any keys and associated data here
        """
        udict = {}
        udict.update(kwargs)
        if d is not None:
            # d is supposed to be a dictionary. Try to update our dict with it
            try:
                udict.update(d)
            # Maybe the user is trying to create a parameter `d`
            except TypeError:
                udict['d'] = d

        self._user_data = udict
        return

    def reset(self):
        """Delete election data for the current run --
        voter preferences, candidate preferences, and ballots,
        Clear the kind of data that can be regenerated if desired.

        Do not clear statistics.
        """
        self.voters.reset()
        self.candidates.reset()

        def delete(a):
            try:
                delattr(self, a)
            except AttributeError:
                pass

        delete('winners')
        delete('ties')
        delete('output')
        delete('vballots')
        raise NotImplementedError('This function probably doesnt work.')
        return

    @utilities.recorder.record_actions(
        replace=True,
        exclude=['ballots', 'result'])
    def run(self,
            etype=None,
            ballots=None,
            result=None,
            force_honest=False) -> ElectionResult:
        """Run the election using `votemethods.eRunner`.

        Parameters
        ----------
        etype : str
            Election method. Either `etype` or `method` must be input.
        ballots : ndarray
            Initial ballots to be used in election.

        result : ElectionResult
            Election, you can input honest election
            using this object to reduce repetitive computation cost.
        force_honest : bool
            Force run of an honest election without strategy
        """

        return self._run(
            etype=etype,
            ballots=ballots,
            result=result,
            force_honest=force_honest
            )

    def _run(self,
            etype=None,
            ballots=None,
            result=None,
            force_honest=False) -> ElectionResult:

        logger.debug('Running %s, %s, %s', etype)
        strategies = self.strategies.data
        if force_honest:
            strategies = ()

        # Auto run an honest election if result is not available.
        elif len(strategies) > 0 and result is None and ballots is None:
            result = self._run(
                etype=etype,
                ballots=None,
                result=None,
                force_honest=True)

        # Retrieve some tactical ballots from honest data.
        ballots, group_index = self.ballotgen.get_ballots(
            etype=etype,
            strategies=strategies,
            result=result,
            ballots=ballots
            )
        runner = votemethods.eRunner(
            etype=etype,
            numwinners=self.numwinners,
            ballots=ballots,
            rstate=self._randomstate,
            )
        self.data = ElectionData(
            ballots=runner.ballots,
            winners=runner.winners,
            ties=runner.ties,
            group_index=group_index
            )

        self.result = self._result_calc.update(
            runner=runner,
            voters=self.voters.data,
            candidates=self.candidates.data,
            election=self.data
            )
        return self.result

    def rerun(self, d):
        """Re-run an election found in dataframe. Find the election
        data from the dataframe index

        Parameters
        ----------
        index : int or None
            Election index from self._dataframe
        d : dict or None
            Dictionary or Series of election data, generated from self.dataseries.

        Returns
        -------
        out : Election
            Newly constructed election object with re-run parameters.
        """
        series = d
        def filterdict(d, kfilter):
            new = {}
            num = len(kfilter)
            for k, v in d.items():
                if k.startswith(kfilter):
                    newkey = k[num :]
                    new[newkey] = v
            return new

        filter_key = 'args.candidate.'
        c_dict = filterdict(series, filter_key)

        filter_key = 'args.election.'
        e_dict = filterdict(series, filter_key)

        filter_key = 'args.strategy.'
        s_dict = filterdict(series, filter_key)

        # Construct voters
        vnum = len(self.voters.group)
        new_voters = []
        for ii in range(vnum):
            filter_key = 'args.voter-%s.' % ii
            v_dict = filterdict(series, filter_key)
            v = type(self.voters.group[ii])()

            #v = type(self.voters)()
            v._method_records.reset()
            v._method_records.run_dict(v_dict, v)
            new_voters.append(v)

        # Construct candidates
        c = type(self.candidates)(voters=new_voters)
        c._method_records.reset()
        c._method_records.run_dict(c_dict, c)

        # Construct strategies
        slen = len(s_dict)
        if slen > 0:
            s = type(self.strategies)

        enew = Election(voters=v, candidates=c)
        enew._method_records.run_dict(e_dict, enew)
        return enew

    def copy(self) -> 'Election':
        """Copy election"""
        return copy.copy(self)

    def save(self, name, reset=True):
        """Pickle election data

        Parameters
        ----------
        name : str
            Name of new pickle file to dump Election ito
        reset : bool
            If True (default), delete election data that can be regenerated.
        """
        if reset:
            self.reset()

        with open(name, 'wb') as file1:
            pickle.dump(self, file1)
        return

    def dataseries(self, index=None):
        """Retrieve pandas data series of output data."""
        return self._result_calc.dataseries(index=index)

    def dataframe(self):
        """Construct data frame from results history."""
        return self._result_calc.dataframe()

    def append_stat(self, d: metrics.BaseStats, name='', update_docs=False):
        return self._result_calc.append_stat(d=d,
                                             name=name,
                                             update_docs=update_docs)

def calculate_distance(voters: VoterData, candidates: CandidateData):
    """Re-calculate distance as the distance from Election may have error."""
    distances = vcalcs.voter_distances(
                                        voters=voters.pref,
                                        candidates=candidates.pref,
                                        weights=voters.weights,
                                        order=voters.order,
                                        )
    return distances

class ElectionResultCalc(object):
    """
    Store Election result output. Generated as attribute of Election.
    This is a sort of messy back-end that does all the calculations. The
    result front end is `ElectionResult`.

    Parameters
    ----------
    e : Election
        Election to extract results from.

    Attributes
    ----------
    runner : :class:`~votesim.votemethods.voterunner.eRunner`
        Output from election running class for the last run election.
    results : dict
        Results of last run election key prefixes:

            - 'output.*' -- Prefix for election output results
            - 'args.etype' -- Election method
            - 'args.voter.*' -- Voter input arguments
            - 'args.election.*' -- Election input arguments
            - 'args.user.*' -- User defined input arguments

    Output Specification
    --------------------
    For each election output keys are generated as dataframes or dataseries.

    - Voter parameters are specified as `args.voter-vnum.a.func.argname`

     - `vnum` = Voter group number
     - `a` = Method call number (a method could be called multiple times.)
     - `func` = Name of the called method
     - `argname` = Name of the set parameter for the method.

    - Candidate parameters are specified as `args.candidate.a.func.arg`
    - User parameters are specified as `args.user.name`
     - `name` is the user's inputted parameter name

    """
    def __init__(self, e: Election):
        self.election = e
        self.save_args = e.save_args
        # Store results as list of dict
        self._output_history = []
        return

    def update(self,
               runner: votemethods.eRunner,
               voters: VoterData,
               candidates: CandidateData,
               election: ElectionData) -> ElectionResult:
        """Get election results."""
        self.runner = runner
        self.winners = runner.winners
        self.ties = runner.ties
        self.ballots = runner.ballots
        self.electionStats = metrics.ElectionStats(voters=voters,
                                                   candidates=candidates,
                                                   election=election)

        ### Build dictionary of all arguments and output
        output = {}
        output.update(self._get_parameters())
        output['output'] = self.electionStats.get_dict()
        output = utilities.misc.flatten_dict(output, sep='.')
        self.output = output

        if self.election.save_records:
            self._output_history.append(output)
        result =  ElectionResult(winners=self.winners,
                                 ties=self.ties,
                                 ballots=self.ballots,
                                 runner=self.runner,
                                 output=self.output,
                                 output_docs=self.output_docs,
                                 stats=self.electionStats,
                                 scoremax=self.election.scoremax
                                 )
        return result

    def _get_parameter_keys(self) -> list:
        """Retrieve election input parameter keys."""
        return list(self._get_parameters().keys())

    def _get_method_records(self) -> dict:
        """Retrieve records that can be used to regenerate result."""
        candidates = self.election.candidates
        voters = self.election.voters
        strategies = self.election.strategies
        election = self.election

        # get voter parameters
        vrecords = []
        for v in voters.group:
            vrecords.append(v._method_records.dict)

        # get candidate parameters
        crecord = candidates._method_records.dict

        # get strategy parameters
        if hasattr(strategies, '_method_records'):
            srecord = strategies._method_records.dict
        else:
            srecord = {}

        # get election parameters
        erecord = election._method_records.dict

        # Save etype and name in special parameters
        params = {}
        for key in erecord:
            if 'run.etype' in key:
                params['args.etype'] = erecord[key]
            elif '.init.name' in key:
                params['args.name'] = erecord[key]

        # Save all method call arguments
        if self.save_args:
            params['args.candidate'] = crecord
            if len(srecord) > 0:
                params['args.strategy'] = srecord
            for ii, vrecord in enumerate(vrecords):
                params['args.voter-%s' % ii] = vrecord
            params['args.election'] = erecord
        return params

    def _get_user_data(self) -> dict:
        # Retrieve user data
        # Determine if user data exists. If not, save default save_args

        try:
            userdata = self.election._user_data
            if len(userdata)  == 0:
                userdata = {}
        except AttributeError:
            userdata = {}

        params = {}
        # Add user data to params
        for key, value in userdata.items():
            newkey = 'args.user.' + key
            params[newkey] = value
        return params

    def _get_parameters(self) -> dict:
        d1 = self._get_user_data()
        d2 = self._get_method_records()
        d1.update(d2)
        return d1

    # def ___get_parameters(self) -> dict:
    #     """Retrieve election input parameters."""
    #     params = {}
    #     candidates = self.election.candidates
    #     voters = self.election.voters
    #     election = self.election

    #     # get candidate parameters
    #     crecord = candidates._method_records.dict

    #     # get voter parameters
    #     vrecords = []
    #     for v in voters.group:
    #         vrecords.append(v._method_records.dict)

    #     # get election parameters
    #     erecord = election._method_records.dict

    #     # Retrieve user data
    #     # Determine if user data exists. If not, save default save_args
    #     save_args = self.save_args
    #     try:
    #         userdata = self.election._user_data
    #         if len(userdata)  == 0:
    #             save_args = True
    #     except AttributeError:
    #         save_args = True
    #         userdata = {}

    #     # Add user data to params
    #     for key, value in userdata.items():
    #         newkey = 'args.user.' + key
    #         params[newkey] = value

    #     # Save etype and name in special parameters
    #     for key in erecord:
    #         if 'run.etype' in key:
    #             params['args.etype'] = erecord[key]
    #         elif '.init.name' in key:
    #             params['args.name'] = erecord[key]

    #     # Save all method call arguments
    #     if self.save_args or save_args:
    #         params['args.candidate'] = crecord
    #         for ii, vrecord in enumerate(vrecords):
    #             params['args.voter-%s' % ii] = vrecord
    #         params['args.election'] = erecord

    #     params = utilities.misc.flatten_dict(params, sep='.')
    #     return params

    @utilities.lazy_property
    def output_docs(self) -> dict:
        """Retrieve output documentation."""
        docs = self.electionStats.get_docs()
        docs = utilities.misc.flatten_dict(docs, sep='.')
        return docs

    def dataseries(self, index=None):
        """Retrieve pandas data series of output data."""
        if index is None:
            return pd.Series(self.output)
        else:
            return pd.Series(self._output_history[index])

    def dataframe(self):
        """Construct data frame from results history."""

        series = []
        for r in self._output_history:
            series.append(pd.Series(r))
        df = pd.concat(series, axis=1, ignore_index=True).transpose()
        df = df.reset_index(drop=True)
        self._dataframe = df.infer_objects()
        return df

    def append_stat(self, d: metrics.BaseStats, name='', update_docs=False):
        """Append custom user stat object to the last result entry.

        Parameters
        ----------
        d : subtype of `metrics.BaseStats` or dict
            Additional outputs to add to the result.
        name : str
            Optional, name of outputs.
        """
        try:
            dict1 = d._dict
            docs1 = d._docs
            name1 = d._name
        except AttributeError:
            dict1 = d
            name1 = name
            docs1 = {}

        dict1 = {'output.' + name1 : dict1}
        dict1 = utilities.misc.flatten_dict(dict1, sep='.')

        result = self._output_history[-1]
        for key in dict1:
            if key in result:
                s = 'Duplicate output key "%s" found for custom stat.' % key
                raise ValueError(s)
        result.update(dict1)
        return

class ResultRecord(object):
    """Store election results here."""
    def __init__(self):
        self.output_history = []

    def append(self, result: ElectionResult):
        output = result.output
        self.output = output
        self.output_history.append(output)
        return

    def dataseries(self, index=None):
        """Retrieve pandas data series of output data."""
        if index is None:
            return pd.Series(self.output)
        else:
            return pd.Series(self.output_history[index])

    def dataframe(self):
        """Construct data frame from results history."""

        series = []
        for r in self.output_history:
            series.append(pd.Series(r))
        df = pd.concat(series, axis=1, ignore_index=True).transpose()
        df = df.reset_index(drop=True)
        self._dataframe = df.infer_objects()
        return df

    def append_stat(self, d: metrics.BaseStats, name='', update_docs=False):
        """Append custom user stat object to the last result entry.

        Parameters
        ----------
        d : subtype of `metrics.BaseStats` or dict
            Additional outputs to add to the result.
        name : str
            Optional, name of outputs.
        """
        try:
            dict1 = d._dict
            docs1 = d._docs
            name1 = d._name
        except AttributeError:
            dict1 = d
            name1 = name
            docs1 = {}

        dict1 = {'output.' + name1 : dict1}
        dict1 = utilities.misc.flatten_dict(dict1, sep='.')

        result = self.output_history[-1]
        for key in dict1:
            if key in result:
                s = 'Duplicate output key "%s" found for custom stat.' % key
                raise ValueError(s)
        result.update(dict1)
        return

reason: import numpy,import scipy,from scipy


text:
import PySimpleGUI as sg
import serial, serial.tools.list_ports
import sys, math

serial_port = None

comms_layout = [
	[
		sg.Text('Port'),
		sg.Combo(values=[], key='-PORT-', size=(30, 1), disabled=True), # Note can be in one of readonly or disabled or enabled.
		sg.Button('Refresh Ports List', size=(7, 2), key='-REFRESH-'),
		sg.Button('Connect', size=(9, 1), key='-CONNECT-'),
		sg.Button('Clear Log', key='-CLEAR-LOG-'),
        sg.Checkbox('Log raw data', key='CB-LOG-RAW-DATA'),
        ],
	[
                sg.Multiline('', key='-LOG-', size=(None, 3), autoscroll=True, auto_refresh=True, write_only=True)
        ],
]

# Scaling for joystick vales which are 24 bit signed so approx +/-8.34e6 fullscale.
JS_SCALING = [int(2**23 / math.sqrt(10)**x) for x in range(7)]
JS_SCALING_TEXT = ('min',) + tuple(map(str,range(1, len(JS_SCALING)-1))) + ('max',)
JS_SCALING_VALUES = dict(zip(JS_SCALING_TEXT, JS_SCALING))

# GUI elements are scaled to this value, progress bar 0..max, graph canvas -max..max.
GUI_MAX = 1000.0

js_layout = [ [
	sg.Text('Sensitivity', size=(13,1), justification='right', font='Helvetica 7'),
	sg.Button('Zero', pad=((10,5),0), key='-ZERO-'),
	sg.Text('Filter'), sg.Combo(values=[str(x) for x in range(5)], readonly=True, size=(4, 1), default_value='0', enable_events=True, key='-FILTER-'),
	sg.Text('Relay %FS'),  sg.Combo(values=[str(x) for x in range(5, 105, 5)], readonly=True, size=(4, 1), default_value='50', enable_events=True, key='-RELAY-'), # Range 5%..100%
] ] + [
	[
		sg.Text(z, size=(1, 1)),
		sg.Combo(values=JS_SCALING_TEXT, default_value=JS_SCALING_TEXT[len(JS_SCALING_TEXT)//2], metadata=JS_SCALING_VALUES, readonly=True, size=(7, 1),
		  enable_events=True, key=f'-{z}-SCALE-'),
        sg.Checkbox('Sound', key=f'-{z}-SOUND-', disabled=z not in 'XY'),
		sg.ProgressBar(GUI_MAX, orientation='h', size=(35, 20), bar_color=col, key=f'-{z}-BARGRAPH-'),
		sg.Text(size=(7, 1), background_color='white', text_color='black', key=f'-{z}-VALUE-'),
	] for z, col in zip('XYZ', (('red', 'IndianRed1'), ('green', 'lightgreen'), ('black', 'grey')))
]

graph_layout = [ [ sg.Graph(canvas_size=(300, 300), graph_bottom_left=(-GUI_MAX, -GUI_MAX), graph_top_right=(GUI_MAX, GUI_MAX), background_color='white', enable_events=False, key='graph')] ]
layout = [
	[ sg.Frame('Serial Port', comms_layout) ],
	[ sg.Frame('Joystick', js_layout) ],
	[ sg.Column(graph_layout, vertical_alignment='center', justification='center'), ],
]

win = sg.Window('TADTas Zero Force Joystick Evaluator', layout, finalize=True)

def log(*args, **kwargs):
	print(*args, **kwargs, file=sys.stderr)
	win['-LOG-'].print(*args, **kwargs)

def send_js_command(cmd):
	if serial_port is not None:
		log(f"Sent command `{cmd}'.")
		try:
			serial_port.write((cmd + '\r').encode('ascii'))
		except serial.serialutil.SerialException:
			pass

# List of ports held in combo box element.
def refresh_ports():
	port_list = [(x.device, x.description) for x in sorted(serial.tools.list_ports.comports()) if 0x2341 != x.vid]
	port_descriptions = [p[1] for p in port_list]
	log(f"Serial ports: {', '.join(port_descriptions)}")
	win['-PORT-'].update(values=port_descriptions, set_to_index=0)
	win['-PORT-'].metadata = {p[1]: p[0] for p in port_list}
	if len(port_list) < 1:
		win['-PORT-'].update(disabled=True)
	else:
		win['-PORT-'].update(readonly=True)
	refresh_connect_button()

def refresh_connect_button():
	win['-CONNECT-'].update(text="Connect" if serial_port is None else "Disconnect", disabled=not win['-PORT-'].metadata)

def do_action_filter(f):
	send_js_command(f'{f} 3 v!')
def do_zero_action():
	send_js_command('js-z')

def close_serial_port():
	global serial_port
	if serial_port is not None:
		try: serial_port.close()
		except serial.serialutil.SerialException: pass
		serial_port = None

# Globals to set pitches on L,R channels.
s_f = [0, 0]
def mute():
	global s_f
	s_f = [0, 0]

def do_connect_action(port_description):
	global serial_port
	if serial_port is None:
		comport_device = win['-PORT-'].metadata[port_description]
		try:
			serial_port = serial.Serial(comport_device, baudrate=115200, timeout=0.01)
		except serial.serialutil.SerialException:
			close_serial_port()
			log(f"Connect failed on {comport_device}.")
		else:
			log(f"Connect OK on {comport_device}.")
			send_js_command('0 relay')			# Turn off relays.
			send_js_command('1 js-dump')		# Turn on JS value dump.
			do_action_filter(win['-FILTER-'].get())
			do_zero_action()
			for z in 'XYZ':
				do_action_rescale(z)
	else:
		send_js_command('0 js-dump')		# Turn off dump.
		send_js_command('0 relay')			# Turn off relays.
		log(f"Closed {serial_port.port}.")
		close_serial_port()
		mute()
	refresh_connect_button()

def get_scale(z):
	scale_descr = win[f'-{z}-SCALE-'].get()
	return scale_descr, win[f'-{z}-SCALE-'].metadata[scale_descr]

S_F = 261.5, 261.5*math.sqrt(2)	# Middle pitch, we've chosen middle C & G above.
# 1.00000, 1.12242, 1.25991, 1.33482, 1.49830, 1.68176, 1.88770, 1.99996 "C" major scale.
S_S = 1.12242, 1.25991, 1.33482, 1.49830, 1.68176,
SOUND_DEADBAND = .02 # If within this band around zero then no sound.
old_relay_val = -1
def do_update_values_action(vals):
	assert(len(vals) == 3)		# Three shalt be the number of the counting...

	# Scale raw values according to scaling setting on GUI.
	scaled_vals = [float(v) / get_scale(z)[1] for z, v in zip('XYZ', vals)]

	# Update each axis bargraph & value element.
	for z, v, vv in zip('XYZ', vals, scaled_vals):
		win[f'-{z}-BARGRAPH-'].update(int((1.0 + vv) * GUI_MAX / 2.0 + 0.5))
		win[f'-{z}-VALUE-'].update(value=str(v))

	# Update XY plot.
	CURSOR_CIRCLE,CURSOR_CROSSHAIR,CURSOR_LINEWIDTH = GUI_MAX/10,GUI_MAX/5, 2
	win['graph'].erase()
	X,Y = scaled_vals[0] * GUI_MAX, scaled_vals[1] * GUI_MAX
	win['graph'].draw_circle((X,Y), CURSOR_CIRCLE, line_color='black', line_width=CURSOR_LINEWIDTH)
	win['graph'].draw_line((X-CURSOR_CROSSHAIR,Y), (X+CURSOR_CROSSHAIR,Y), color='black', width=CURSOR_LINEWIDTH)
	win['graph'].draw_line((X,Y-CURSOR_CROSSHAIR), (X,Y+CURSOR_CROSSHAIR), color='black', width=CURSOR_LINEWIDTH)

	# Update sounds.
	global s_f
	M = len(S_S)-1
	ff = [0, 0]
	for i in range(2):
		if abs(scaled_vals[i]) >= SOUND_DEADBAND and win['-%s-SOUND-'%('XY'[i])].get():	# If out of detent...
			rf = math.copysign((abs(scaled_vals[i]) - SOUND_DEADBAND) / (1 - SOUND_DEADBAND), scaled_vals[i]) # Range 0.0..1.0
			sign = scaled_vals[i] >= 0
			ff[i] = max(min(int(rf*(M+1)), M), -M)
			s_f[i] = S_F[i] * (S_S[ff[i]] if sign else 1.0 / S_S[-ff[i]])
		else:
			s_f[i] = 0
			ff[i] = None
	# log(f"Sound: {ff}.")

	# Update relays
	relay_threshold = float(win['-RELAY-'].get()) / 100.0
	relay_val = 0
	if scaled_vals[0] > +relay_threshold:
		relay_val = 1
	elif scaled_vals[0] < -relay_threshold:
		relay_val = 2
	if scaled_vals[1] > +relay_threshold:
		relay_val |= 4
	if scaled_vals[1] < -relay_threshold:
		relay_val |= 8
	global old_relay_val
	if old_relay_val != relay_val:
		send_js_command(f'{relay_val} relay')
		old_relay_val = relay_val

raw_line = b''
def do_read_joystick():
	global raw_line
	if serial_port is not None:
		try:
			raw_line = raw_line + serial_port.readline()
		except serial.serialutil.SerialException:
			close_serial_port()
			log("Serial port exception, port closed.")
			refresh_connect_button()
		else:
			if raw_line:
				if raw_line[-1] == 10:
					try:
						line = raw_line.decode('ascii').strip()
					except ValueError:
						log(f"Could not decode `{raw_line}'.")
					else:
						if win['CB-LOG-RAW-DATA'].get():
							log(line)
						if line.startswith("JS:"):
							vals = line.split()
							if len(vals) != 5:
								log("Invalid format.")
							else:
								do_update_values_action(list(map(int, vals[2:5])))
					raw_line = b''

def do_action_rescale(z):
	scale_descr, scale = get_scale(z)
	log(f"Fullscale for axis {z} set to \u00b1{scale:.3g} ({scale_descr}).")

# Sound stuff.

import numpy as np
import sounddevice as sd

AMPLITUDE = 0.4

DEVICE = None	# Seems to use speaker as a default.
SAMPLERATE = sd.query_devices(DEVICE, 'output')['default_samplerate']

def mk_samples(t, f):
	return AMPLITUDE * np.sin(2 * np.pi * f * t)

start_idx = 0
s_ff = [0, 0]
def cb_sound(outdata, frames, time, status):
	global start_idx
	if status:
		print(status, file=sys.stderr)
	t = (start_idx + np.arange(frames)) / SAMPLERATE
	t = t.reshape(-1, 1)
	outdata[:] = list(zip(mk_samples(t, s_f[0]), mk_samples(t, s_f[1])))
	start_idx += frames

## Initialise.
refresh_ports()

stros = sd.OutputStream(device=DEVICE, channels=2, callback=cb_sound, samplerate=SAMPLERATE)
stros.start()

while True:
	e, v = win.read(timeout=20)
	if e == '__TIMEOUT__':
		do_read_joystick()
		continue
	print(e, v)
	if e in (sg.WIN_CLOSED, 'Exit'):
		break
	elif e == '-REFRESH-':
		refresh_ports()
	elif e == '-CLEAR-LOG-':
		win['-LOG-'].update(value='')
	elif e == '-CONNECT-':
		do_connect_action(v['-PORT-'])
	elif e == '-ZERO-':
		do_zero_action()
	elif e == '-FILTER-':
		do_action_filter(v['-FILTER-'])
	elif e == '-X-SCALE-':
		do_action_rescale('X')
	elif e == '-Y-SCALE-':
		do_action_rescale('Y')
	elif e == '-Z-SCALE-':
		do_action_rescale('Z')

win.close()

ext: py

max_stars_repo_name: celephicus/tadtas-joystick

max_issues_repo_name: celephicus/tadtas-joystick

max_forks_repo_name: celephicus/tadtas-joystick

reason: import numpy


text:
"""
Environments and wrappers for Sonic training.
"""
from collections import deque

import gym
from gym import Env, error, spaces
import numpy as np

from plangym.env import AtariEnvironment, Environment

class Wrapper(gym.Env):
    env = None

    def __init__(self, env):
        self.env = env
        self.action_space = self.env.action_space
        self.observation_space = self.env.observation_space
        self.reward_range = self.env.reward_range
        self.metadata = self.env.metadata
        self._warn_double_wrap()

    @classmethod
    def class_name(cls):
        return cls.__name__

    def __getattr__(self, item):
        return getattr(self.env, item)

    def _warn_double_wrap(self):
        env = self.env
        while True:
            if isinstance(env, Wrapper):
                if env.class_name() == self.class_name():
                    raise error.DoubleWrapperError(
                        "Attempted to double wrap with Wrapper: "
                        "{}".format(self.__class__.__name__)
                    )
                env = env.env
            else:
                break

    def step(self, action, *args, **kwargs):
        if isinstance(self.env, Environment):
            return self.env.step(action, *args, **kwargs)
        else:
            return self.env.step(action)

    def reset(self, **kwargs):
        return self.env.reset(**kwargs)

    def render(self, mode="human"):
        return self.env.render(mode)

    def close(self):
        if self.env:
            return self.env.close()

    def seed(self, seed=None):
        return self.env.seed(seed)

    def compute_reward(self, achieved_goal, desired_goal, info):
        return self.env.compute_reward(achieved_goal, desired_goal, info)

    def __str__(self):
        return "<{}{}>".format(type(self).__name__, self.env)

    def __repr__(self):
        return str(self)

    @property
    def unwrapped(self):
        return self.env.unwrapped

    @property
    def spec(self):
        return self.env.spec

class ActionWrapper(Wrapper):
    def step(self, action, *args, **kwargs):
        action = self.action(action)
        if isinstance(self.env, Environment):
            return self.env.step(action, *args, **kwargs)
        else:
            return self.env.step(action)

    def reverse_action(self, action):
        return self._reverse_action(action)

class AtariWrapper(AtariEnvironment):

    """Generic wrapper for the AtariEnvironment. Inherit from this class and override
     the desired methods to implement a wrapper."""

    def __init__(
        self,
        env: Env,
        clone_seeds: bool = True,
        n_repeat_action: int = 1,
        min_dt: int = 1,
        obs_ram: bool = False,
        episodic_live: bool = False,
        autoreset: bool = True,
    ):
        """Create a wrapper for the AtariEnvironment.
        :param clone_seeds: Clone the random seed of the ALE emulator when reading/setting
         the state.
        :param n_repeat_action: Consecutive number of times a given action will be applied.
        :param min_dt: Internal number of times an action will be applied for each step in
         n_repeat_action.
        :param obs_ram: Use ram as observations even though it is not specified elsewhere.
        :param episodic_live: Return end = True when losing a live.
        :param autoreset: Restart environment when reaching a terminal state.
        :param env: Instance of AtariEnvironment, or a wrapped AtariEnvironment.
        """
        super(AtariWrapper, self).__init__(
            name=env.spec.id,
            clone_seeds=clone_seeds,
            n_repeat_action=n_repeat_action,
            min_dt=min_dt,
            obs_ram=obs_ram,
            episodic_live=episodic_live,
            autoreset=autoreset,
        )
        # Overwrite the default env with the one provided externally
        self._env = env
        self.action_space = self._env.action_space
        self.observation_space = self._env.observation_space
        self.reward_range = self._env.reward_range
        self.metadata = self._env.metadata

class SonicDiscretizer(ActionWrapper):
    """
    Wrap a gym-retro environment and make it use discrete
    actions for the Sonic game.
    """

    def __init__(self, env):
        super(SonicDiscretizer, self).__init__(env)
        buttons = ["B", "A", "MODE", "START", "UP", "DOWN", "LEFT", "RIGHT", "C", "Y", "X", "Z"]
        actions = [
            ["LEFT"],
            ["RIGHT"],
            ["LEFT", "DOWN"],
            ["RIGHT", "DOWN"],
            ["DOWN"],
            ["DOWN", "B"],
            ["B"],
        ]
        self._actions = []
        for action in actions:
            arr = np.array([False] * 12)
            for button in action:
                arr[buttons.index(button)] = True
            self._actions.append(arr)
        self.action_space = gym.spaces.Discrete(len(self._actions))

    def action(self, a):  # pylint: disable=W0221
        return self._actions[a].copy()

class RewardScaler(gym.RewardWrapper):
    """
    Bring rewards to a reasonable scale for PPO.
    This is incredibly important and effects performance
    drastically.
    """

    def reward(self, reward):
        return reward * 0.01

class LazyFrames(object):
    def __init__(self, frames):
        """This object ensures that common frames between the observations are only stored once.
        It exists purely to optimize memory usage which can be huge for DQN's 1M frames replay
        buffers.

        This object should only be converted to numpy array before being passed to the model.

        You'd not believe how complex the previous solution was."""
        self._frames = frames
        self._out = None

    def _force(self):
        if self._out is None:
            self._out = np.concatenate(self._frames, axis=2)
            self._frames = None
        return self._out

    def __array__(self, dtype=None):
        out = self._force()
        if dtype is not None:
            out = out.astype(dtype)
        return out

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

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

class FrameStack(gym.Wrapper):
    def __init__(self, env, k):
        """Stack k last frames.

        Returns lazy array, which is much more memory efficient.

        See Also
        --------
        baselines.common.atari_wrappers.LazyFrames
        """
        gym.Wrapper.__init__(self, env)
        self.k = k
        self.frames = deque([], maxlen=k)
        shp = env.observation_space.shape
        self.observation_space = spaces.Box(
            low=0, high=255, shape=(shp[0], shp[1], shp[2] * k), dtype=np.uint8
        )

    def __getattr__(self, item):
        return getattr(self.env, item)

    def reset(self):
        ob = self.env.reset()
        for _ in range(self.k):
            self.frames.append(ob)
        return self._get_ob()

    def step(self, action):
        ob, reward, done, info = self.env.step(action)
        self.frames.append(ob)
        return self._get_ob(), reward, done, info

    def _get_ob(self):
        assert len(self.frames) == self.k
        return LazyFrames(list(self.frames))

max_stars_repo_name: justindujardin/plangym

max_issues_repo_name: justindujardin/plangym

max_forks_repo_name: justindujardin/plangym


text:
"""
Stores the class for ContourDisplay.

"""

import numpy as np
from scipy.interpolate import Rbf

# Import Local Libs
from .plot import Display

class ContourDisplay(Display):
    """
    This subclass contains routines that are specific to plotting
    contour plots from data. It is inherited from Display and therefore
    contains all of Display's attributes and methods.

    """

    def __init__(self, obj, subplot_shape=(1,), ds_name=None, **kwargs):
        super().__init__(obj, subplot_shape, ds_name, **kwargs)

    def create_contour(
        self,
        fields=None,
        time=None,
        function='cubic',
        subplot_index=(0,),
        contour='contourf',
        grid_delta=(0.01, 0.01),
        grid_buffer=0.1,
        twod_dim_value=None,
        **kwargs,
    ):
        """
        Extracts, grids, and creates a contour plot. If subplots have not been
        added yet, an axis will be created assuming that there is only going
        to be one plot.

        Parameters
        ----------
        fields : dict
            Dictionary of fields to use for x, y, and z data.
        time : datetime
            Time in which to slice through objects.
        function : string
            Defaults to cubic function for interpolation.
            See scipy.interpolate.Rbf for additional options.
        subplot_index : 1 or 2D tuple, list, or array
            The index of the subplot to set the x range of.
        contour : str
            Whether to use contour or contourf as plotting function.
            Default is contourf
        grid_delta : 1D tuple, list, or array
            x and y deltas for creating grid.
        grid_buffer : float
            Buffer to apply to grid.
        twod_dim_value : float
            If the field is 2D, which dimension value to pull.
            I.e. if dim is depths of [5, 10, 50, 100] specifying 50
            would index the data at 50
        **kwargs : keyword arguments
            The keyword arguments for :func:`plt.contour`

        Returns
        -------
        ax : matplotlib axis handle
            The matplotlib axis handle of the plot.

        """
        # Get x, y, and z data by looping through each dictionary
        # item and extracting data from appropriate time
        x = []
        y = []
        z = []
        for ds in self._obj:
            obj = self._obj[ds]
            if ds not in fields:
                continue
            field = fields[ds]
            if obj[field[2]].sel(time=time).values.size > 1:
                dim_values = obj[obj[field[2]].dims[1]].values
                if twod_dim_value is None:
                    dim_index = 0
                else:
                    dim_index = np.where(dim_values == twod_dim_value)
                if dim_index[0].size == 0:
                    continue
                if np.isnan(obj[field[2]].sel(time=time).values[dim_index]):
                    continue
                z.append(obj[field[2]].sel(time=time).values[dim_index].tolist())
            else:
                if np.isnan(obj[field[2]].sel(time=time).values):
                    continue
                z.append(obj[field[2]].sel(time=time).values.tolist())

            if obj[field[0]].values.size > 1:
                x.append(obj[field[0]].sel(time=time).values.tolist())
            else:
                x.append(obj[field[0]].values.tolist())

            if obj[field[1]].values.size > 1:
                y.append(obj[field[1]].sel(time=time).values.tolist())
            else:
                y.append(obj[field[1]].values.tolist())

        # Create a meshgrid for gridding onto
        xs = np.arange(np.min(x) - grid_buffer, np.max(x) + grid_buffer, grid_delta[0])
        ys = np.arange(np.min(y) - grid_buffer, np.max(y) + grid_buffer, grid_delta[1])
        xi, yi = np.meshgrid(xs, ys)

        # Use scipy radial basis function to interpolate data onto grid
        rbf = Rbf(x, y, z, function=function)
        zi = rbf(xi, yi)

        # Create contour plot
        if contour == 'contour':
            self.contour(xi, yi, zi, subplot_index=subplot_index, **kwargs)
        elif 'contourf':
            self.contourf(xi, yi, zi, subplot_index=subplot_index, **kwargs)
        else:
            raise ValueError(
                "Invalid contour plot type. Please choose either 'contourf' or 'contour'"
            )

        return self.axes[subplot_index]

    def contourf(self, x, y, z, subplot_index=(0,), **kwargs):
        """
        Base function for filled contours if user already has data gridded.
        If subplots have not been added yet, an axis will be created
        assuming that there is only going to be one plot.

        Parameters
        ----------
        x : array-like
            x coordinates or grid for z.
        y : array-like
            y coordinates or grid for z.
        z : array-like(x,y)
            Values over which to contour.
        subplot_index : 1 or 2D tuple, list, or array
            The index of the subplot to set the x range of.
        **kwargs : keyword arguments
            The keyword arguments for :func:`plt.contourf`

        Returns
        -------
        ax : matplotlib axis handle
            The matplotlib axis handle of the plot.

        """
        self.axes[subplot_index].contourf(x, y, z, **kwargs)

        return self.axes[subplot_index]

    def contour(self, x, y, z, subplot_index=(0,), **kwargs):
        """
        Base function for contours if user already has data gridded.
        If subplots have not been added yet, an axis will be created
        assuming that there is only going to be one plot.

        Parameters
        ----------
        x : array-like
            x coordinates or grid for z.
        y : array-like
            y coordinates or grid for z.
        z : array-like(x, y)
            Values over which to contour.
        subplot_index : 1 or 2D tuple, list, or array
            The index of the subplot to set the x range of.
        **kwargs : keyword arguments
            The keyword arguments for :func:`plt.contour`

        Returns
        -------
        ax : matplotlib axis handle
            The matplotlib axis handle of the plot.

        """
        self.axes[subplot_index].contour(x, y, z, **kwargs)

        return self.axes[subplot_index]

    def plot_vectors_from_spd_dir(
        self,
        fields,
        time=None,
        subplot_index=(0,),
        mesh=False,
        function='cubic',
        grid_delta=(0.01, 0.01),
        grid_buffer=0.1,
        **kwargs,
    ):
        """
        Extracts, grids, and creates a contour plot.
        If subplots have not been added yet, an axis will be created
        assuming that there is only going to be one plot.

        Parameters
        ----------
        fields : dict
            Dictionary of fields to use for x, y, and z data.
        time : datetime
            Time in which to slice through objects.
        mesh : boolean
            Set to True to interpolate u and v to grid and create wind barbs.
        function : string
            Defaults to cubic function for interpolation.
            See scipy.interpolate.Rbf for additional options.
        grid_delta : 1D tuple, list, or array
            x and y deltas for creating grid.
        grid_buffer : float
            Buffer to apply to grid.
        **kwargs : keyword arguments
            The keyword arguments for :func:`plt.barbs`

        Returns
        -------
        ax : matplotlib axis handle
            The matplotlib axis handle of the plot.

        """
        # Get x, y, and z data by looping through each dictionary
        # item and extracting data from appropriate time
        x = []
        y = []
        wspd = []
        wdir = []
        for ds in self._obj:
            obj = self._obj[ds]
            field = fields[ds]
            if obj[field[0]].values.size > 1:
                x.append(obj[field[0]].sel(time=time).values.tolist())
            else:
                x.append(obj[field[0]].values.tolist())

            if obj[field[1]].values.size > 1:
                y.append(obj[field[1]].sel(time=time).values.tolist())
            else:
                y.append(obj[field[1]].values.tolist())
            wspd.append(obj[field[2]].sel(time=time).values.tolist())
            wdir.append(obj[field[3]].sel(time=time).values.tolist())

        # Calculate u and v
        tempu = -np.sin(np.deg2rad(wdir)) * wspd
        tempv = -np.cos(np.deg2rad(wdir)) * wspd

        if mesh is True:
            # Create a meshgrid for gridding onto
            xs = np.arange(min(x) - grid_buffer, max(x) + grid_buffer, grid_delta[0])
            ys = np.arange(min(y) - grid_buffer, max(y) + grid_buffer, grid_delta[1])
            xi, yi = np.meshgrid(xs, ys)

            # Use scipy radial basis function to interpolate data onto grid
            rbf = Rbf(x, y, tempu, function=function)
            u = rbf(xi, yi)

            rbf = Rbf(x, y, tempv, function=function)
            v = rbf(xi, yi)
        else:
            xi = x
            yi = y
            u = tempu
            v = tempv

        self.barbs(xi, yi, u, v, **kwargs)

        return self.axes[subplot_index]

    def barbs(self, x, y, u, v, subplot_index=(0,), **kwargs):
        """
        Base function for wind barbs. If subplots have not been added yet,
        an axis will be created assuming that there is only going to be
        one plot.

        Parameters
        ----------
        x : array-like
            x coordinates or grid for z.
        y : array-like
            y coordinates or grid for z.
        u : array-like
            U component of vector.
        v : array-like
            V component of vector.
        subplot_index : 1 or 2D tuple, list, or array
            The index of the subplot to set the x range of.
        **kwargs : keyword arguments
            The keyword arguments for :func:`plt.barbs`

        Returns
        -------
        ax : matplotlib axis handle
            The matplotlib axis handle of the plot.

        """
        self.axes[subplot_index].barbs(x, y, u, v, **kwargs)

        return self.axes[subplot_index]

    def plot_station(self, fields, time=None, subplot_index=(0,), text_color='white', **kwargs):
        """
        Extracts, grids, and creates a contour plot. If subplots have not
        been added yet, an axis will be created assuming that there is only
        going to be one plot.

        Parameters
        ----------
        fields : dict
            Dictionary of fields to use for x, y, and z data.
        time : datetime
            Time in which to slice through objects.
        subplot_index : 1 or 2D tuple, list, or array
            The index of the subplot to set the x range of.
        text_color : string
            Color for text.
        **kwargs : keyword arguments
            The keyword arguments for :func:`plt.plot`

        Returns
        -------
        ax : matplotlib axis handle
            The matplotlib axis handle of the plot.

        """
        # Get x, y, and data by looping through each dictionary
        # item and extracting data from appropriate time
        for ds in self._obj:
            obj = self._obj[ds]
            field = fields[ds]
            for i, f in enumerate(field):
                if i == 0:
                    if obj[f].values.size > 1:
                        x = obj[f].sel(time=time).values.tolist()
                    else:
                        x = obj[f].values.tolist()
                elif i == 1:
                    if obj[f].values.size > 1:
                        y = obj[f].sel(time=time).values.tolist()
                    else:
                        y = obj[f].values.tolist()
                    self.axes[subplot_index].plot(x, y, '*', **kwargs)
                else:
                    data = obj[f].sel(time=time).values.tolist()
                    offset = 0.02
                    if i == 2:
                        x1 = x - 3.0 * offset
                        y1 = y + offset
                    if i == 3:
                        x1 = x + offset
                        y1 = y + offset
                    if i == 4:
                        x1 = x + offset
                        y1 = y - 2.0 * offset
                    if i == 5:
                        x1 = x - 3.0 * offset
                        y1 = y - 2.0 * offset
                    if data < 5:
                        string = str(round(data, 1))
                    else:
                        string = str(round(data))
                    self.axes[subplot_index].text(x1, y1, string, color=text_color)

        return self.axes[subplot_index]

reason: import numpy,from scipy


text:
import pandas as pd
import numpy as np
from scipy.stats import chi2, chisquare, fisher_exact, chi2_contingency
from sklearn.metrics import matthews_corrcoef
from Stats.TheilsU import *

def performance_metrics(df, col, target):
    """
    Setting Univariate Benchmarks using DataFrame
    col = feature we are intersted in (str)
    target = what we are predicting
        - col of df as str
        - or can be of type pd.series/np.array
    df: rows= patients, cols = features

    Marvasti Dec 2019
    """

    if type(target) != str:
        df[target.name] = np.nan
        df.loc[target.index, target.name] = target
        target=target.name

    no_of_pts_with_semiology_and_target_outcome = df.loc[df[col]==1, target].sum()
    total_with_semio = df.loc[df[col]==1, target].count()
    total_with_target = df.loc[df[target]==1, col].sum()
    total_patients = df.shape[0]

    col_y_target_y = no_of_pts_with_semiology_and_target_outcome
    col_y_target_n = total_with_semio - no_of_pts_with_semiology_and_target_outcome
    # col_n_target_y = total_with_target - no_of_pts_with_semiology_gold
    col_n_target_y = df.loc[((df[target]==1)&(df[col]==0)), target].count()  # equivalent to .sum()
    col_n_target_n = df.loc[((df[target]==0)&(df[col]==0)), target].count()

    print('variable being tested: ', col, ' target: ', target)
    if col_y_target_n == (df.loc[((df[target]==0)&(df[col]==1)), target].count()) :
        print ('integrity check : pass')
    else:
        print('oops major probelmo: not adding up. see source')

    OR_fisher, pv_fisher = fisher_exact(
        [
        [col_y_target_y, col_y_target_n],
        [col_n_target_y, col_n_target_n]
        ]         )

    OR_chi, pv_chi, _, _ = chi2_contingency(
        [
        [col_y_target_y, col_y_target_n],
        [col_n_target_y, col_n_target_n]
        ]         )

    OR_manual = (col_y_target_y/col_y_target_n) / (col_n_target_y/col_n_target_n)

    SENS = col_y_target_y/(col_y_target_y+col_n_target_y)

    SPEC = col_n_target_n/(col_y_target_n+col_n_target_n)

    PPV = col_y_target_y/(col_y_target_y+col_y_target_n)

    NPV = col_n_target_n/(col_n_target_y+col_n_target_n)

    f1_score_target_y = 2*(SENS)*(PPV)/(SENS+PPV)

    f1_score_target_n = 2*(SPEC)*(NPV)/(SPEC+NPV)

    F1_MACRO = (f1_score_target_y+f1_score_target_n)/2

    BAL_ACC = (SENS+SPEC)/2

    ACCURACY_simple = (col_y_target_y+col_n_target_n)/(col_y_target_y+col_n_target_n+col_y_target_n+col_n_target_y)

    MCC = matthews_corrcoef(df[target], df[col])

    corr_test = associations(df[[col, target]],
                          nominal_columns='all', mark_columns=False, Theils_U=True, plot=False,
                          return_results = True,
                          savefigure=False,
                          title_auto=False)
    THEILSU = corr_test[col][target]

    metrics_OR = {'OR_fisher': OR_fisher, 'OR_chi':OR_chi ,  'OR_manual': OR_manual}
    metrics_classic = {'SENS':SENS, 'SPEC':SPEC, 'PPV':PPV, 'NPV':NPV, 'F1_MACRO':F1_MACRO,  'BAL_ACC':BAL_ACC,
        'ACCURACY_simple':ACCURACY_simple, 'Matthews Correlation Coefficient':MCC, 'Theils U': THEILSU}

    for i in metrics_OR.keys():
        print(i, metrics_OR[i])

    for j in metrics_classic.keys():
        print(j, metrics_classic[j])

max_stars_repo_name: karandahele/Epilepsy-Repository

max_issues_repo_name: karandahele/Epilepsy-Repository

max_forks_repo_name: karandahele/Epilepsy-Repository


text:
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.colors as colors
import math

from matplotlib import cm
from matplotlib import rc
from matplotlib import rcParams

__author__ = 'ernesto'

# if use latex or mathtext
rc('text', usetex=True)
rcParams['text.latex.preamble'] = [r"\usepackage{amsmath}"]

# auxiliar function for plot ticks of equal length in x and y axis despite its scales.
def convert_display_to_data_coordinates(transData, length=10):
    # create a transform which will take from display to data coordinates
    inv = transData.inverted()
    # transform from display coordinates to data coordinates in x axis
    data_coords = inv.transform([(0, 0), (length, 0)])
    # get the length of the segment in data units
    yticks_len = data_coords[1, 0] - data_coords[0, 0]
    # transform from display coordinates to data coordinates in y axis
    data_coords = inv.transform([(0, 0), (0, length)])
    # get the length of the segment in data units
    xticks_len = data_coords[1, 1] - data_coords[0, 1]
    return xticks_len, yticks_len

#####################################
# PARAMETERS - This can be modified #
#####################################

# parámetros

# probabilidad
P = 0.9
c = math.sqrt(2 * math.log(1 / (1 - 0.9)))

print(c)
print(c**2)

# parametros de las elipses calculados de forma teorica
# elipse 1
a1 = c
b1 = c
# elipse 2
a2 = c
b2 = math.sqrt(2) * c
# elipse 3
a3 = math.sqrt(3) * c
b3 = c
theta3 = math.pi / 4

#####################
# END OF PARAMETERS #
#####################

t = np.linspace(0, 2 * math.pi, 300)

# ecuaciones paramétricas de las elipses

# elipse 1
x1 = a1 * np.cos(t)
y1 = b1 * np.sin(t)

# elipse 2
x2 = a2 * np.cos(t)
y2 = b2 * np.sin(t)

# elipse 3
x3 = a3 * np.cos(t) * np.cos(theta3) - b3 * np.sin(t) * np.sin(theta3)
y3 = a3 * np.cos(t) * np.sin(theta3) + b3 * np.sin(t) * np.cos(theta3)

# axis parameters
xmax_ax = 2.2 * c
xmin_ax = -xmax_ax
ymax_ax = 2.2 * c
ymin_ax = -ymax_ax

# length of the ticks for all subplot (6 pixels)
display_length = 6  # in pixels
# x ticks labels margin
xtm = -0.7
ytm = -0.4
# font size
fontsize = 14
# markersize
ms = 9
# colors from coolwarm
cNorm = colors.Normalize(vmin=0, vmax=1)
scalarMap = cm.ScalarMappable(norm=cNorm, cmap=cm.coolwarm)
col10 = scalarMap.to_rgba(0.1)
col20 = scalarMap.to_rgba(1)

f = plt.figure(0, figsize=(10, 3), frameon=False)
ax = plt.subplot2grid((1, 15), (0, 0), rowspan=1, colspan=5)

plt.axis('equal')
plt.xlim(xmin_ax, xmax_ax)
plt.ylim(ymin_ax, ymax_ax)

f.canvas.draw()
ymin_ax, ymax_ax = ax.get_ylim()
xmin_ax, xmax_ax = ax.get_xlim()

# axis arrows
plt.annotate("", xytext=(xmin_ax, 0), xycoords='data', xy=(xmax_ax, 0), textcoords='data',
             arrowprops=dict(width=0.1, headwidth=6, headlength=8, facecolor='black', shrink=0.002), zorder=-1)
plt.annotate("", xytext=(0, ymin_ax), xycoords='data', xy=(0, ymax_ax), textcoords='data',
             arrowprops=dict(width=0.1, headwidth=6, headlength=8, facecolor='black', shrink=0.002), zorder=-1)

# elipse 1
plt.plot(x1, y1, 'k-', lw=2.5)

# focos
plt.plot(0, 0, 'k.', markersize=ms)

# otros puntos
plt.plot(c, 0, 'k.', markersize=ms)

# labels
plt.text(xmax_ax-0.2, -0.8, '$\epsilon_{\hat{a}}$', fontsize=fontsize, ha='right', va='baseline')
plt.text(ytm, ymax_ax-0.3, '$\epsilon_{\hat{b}}$', fontsize=fontsize, ha='right', va='top')
plt.text(c+0.2, xtm, '$c$', fontsize=fontsize, ha='left', va='baseline')

##########################
ax = plt.subplot2grid((1, 15), (0, 5), rowspan=1, colspan=5)

plt.xlim(xmin_ax, xmax_ax)
plt.ylim(ymin_ax, ymax_ax)

# axis arrows
plt.annotate("", xytext=(xmin_ax, 0), xycoords='data', xy=(xmax_ax, 0), textcoords='data',
             arrowprops=dict(width=0.1, headwidth=6, headlength=8, facecolor='black', shrink=0.002), zorder=-1)
plt.annotate("", xytext=(0, ymin_ax), xycoords='data', xy=(0, ymax_ax), textcoords='data',
             arrowprops=dict(width=0.1, headwidth=6, headlength=8, facecolor='black', shrink=0.002), zorder=-1)

# elipse 2
plt.plot(x2, y2, 'k-', lw=2.5)

# focos
plt.plot(0, c, 'k.', markersize=ms)
plt.plot(0, -c, 'k.', markersize=ms)

# otros puntos
plt.plot(c, 0, 'k.', markersize=ms)
plt.plot(0, math.sqrt(2) * c, 'k.', markersize=ms)

# labels
plt.text(xmax_ax-0.2, -0.8, '$\epsilon_{\hat{a}}$', fontsize=fontsize, ha='right', va='baseline')
plt.text(ytm, ymax_ax-0.3, '$\epsilon_{\hat{b}}$', fontsize=fontsize, ha='right', va='top')
plt.text(ytm, c, '$c$', fontsize=fontsize, ha='right', va='center')
plt.text(ytm, -c, '$-c$', fontsize=fontsize, ha='right', va='center')
plt.text(c+0.2, xtm, '$c$', fontsize=fontsize, ha='left', va='baseline')
plt.text(0.2, math.sqrt(2) * c + 0.2, '$\sqrt{2}c$', fontsize=fontsize, ha='left', va='baseline')

ax.set_yticklabels([])

#################
ax = plt.subplot2grid((1, 15), (0, 10), rowspan=1, colspan=5)

plt.xlim(xmin_ax, xmax_ax)
plt.ylim(ymin_ax, ymax_ax)

# axis arrows
plt.annotate("", xytext=(xmin_ax, 0), xycoords='data', xy=(xmax_ax, 0), textcoords='data',
             arrowprops=dict(width=0.1, headwidth=6, headlength=8, facecolor='black', shrink=0.002), zorder=-1)
plt.annotate("", xytext=(0, ymin_ax), xycoords='data', xy=(0, ymax_ax), textcoords='data',
             arrowprops=dict(width=0.1, headwidth=6, headlength=8, facecolor='black', shrink=0.002), zorder=-1)

# elipse 3
plt.plot(x3, y3, 'k-', lw=2.5)

# eje
plt.plot([ymin_ax, ymax_ax], [ymin_ax, ymax_ax], 'k--', lw=1)
plt.plot([ymin_ax, ymax_ax], [ymax_ax, ymin_ax], 'k--', lw=1)

# focos
plt.plot(math.sqrt(2) * c * np.cos(theta3), math.sqrt(2) * c * np.sin(theta3), 'k.', markersize=ms, zorder=3)
plt.plot(-math.sqrt(2) * c * np.cos(theta3), -math.sqrt(2) * c * np.sin(theta3), 'k.', markersize=ms)

# semi-ejes
plt.plot([0, np.sqrt(3) * c * np.cos(theta3)], [0, np.sqrt(3) * c * np.sin(theta3)], 'r-', lw=2, zorder=2)
plt.plot([0, -c * np.cos(theta3)], [0, c * np.sin(theta3)], 'b-', lw=2, zorder=2)

# angle
t1 = np.linspace(0, theta3, 30)
lt = 0.8
xt = lt * np.cos(t1)
yt = lt * np.sin(t1)
plt.plot(xt, yt, 'k', lw=1)

# labels
plt.text(xmax_ax-0.2, -0.8, '$\epsilon_{\hat{a}}$', fontsize=fontsize, ha='right', va='baseline')
plt.text(ytm, ymax_ax-0.3, '$\epsilon_{\hat{b}}$', fontsize=fontsize, ha='right', va='top')
plt.text(lt+0.1, 0.35, '$\pi/4$', fontsize=fontsize, ha='left', va='baseline')
plt.text(0.9, 2, '$\sqrt{3}c$', color='r', fontsize=fontsize, ha='center', va='center')
plt.text(-c*np.cos(theta3)/2+0.2, c * np.sin(theta3)/2+0.3, '$c$', color='b',
         fontsize=fontsize, ha='center', va='center')

plt.annotate(r'$(c,\,c)$', xytext=(0.5, 3.6), xycoords='data', xy=(c, c),
             textcoords='data', color='k', fontsize=fontsize, va="baseline", ha="left",
             arrowprops=dict(arrowstyle="-|>, head_width=0.15, head_length=0.3", color='k', relpos=(0.6, 0),
                             patchA=None, patchB=None, shrinkA=0, shrinkB=3))

ax.set_yticklabels([])

# plt.axis('off')

# save as pdf image
plt.savefig('problem_11_14.pdf', bbox_inches='tight')

plt.show()


text:
"""Categorical sklearn privacy metric modules and their attackers."""

import numpy as np
import sklearn.naive_bayes
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
from sklearn.svm import SVC

from sdmetrics.single_table.privacy.base import (
    CategoricalPrivacyMetric, CategoricalType, PrivacyAttackerModel)
from sdmetrics.single_table.privacy.util import allow_nan, allow_nan_array

class CategoricalSklearnAttacker(PrivacyAttackerModel):
    """Base class for the categorical attackers based on sklearn models.

    It is used to train a model to predict sensitive attributes from key attributes
    using the synthetic data. Then, evaluate the privacy of the model by
    trying to predict the sensitive attributes of the real data.

    Attributes:
        key_type (CategoricalType):
            Required key attribute type (class_num or one_hot) by the learner.
        sensitive_type (CategoricalType):
            Required sensitive attribute type (class_num or one_hot) by the learner.
        skl_learner (Class):
            A (wrapped) sklearn classifier class that can be called with no arguments.
    """

    KEY_TYPE = None
    SENSITIVE_TYPE = None
    SKL_LEARNER = None

    def __init__(self):
        self.predictor = self.SKL_LEARNER()
        self.key_processor = (
            OrdinalEncoder() if self.KEY_TYPE == CategoricalType.CLASS_NUM
            else OneHotEncoder()
        )
        self.sensitive_processor = (
            OrdinalEncoder() if
            self.SENSITIVE_TYPE == CategoricalType.CLASS_NUM else OneHotEncoder()
        )

    def fit(self, synthetic_data, key_fields, sensitive_fields):
        """Fit the CategoricalSklearnAttacker on the synthetic data.

        Args:
            synthetic_data(pandas.DataFrame):
                The synthetic data table used for adverserial learning.
            key_fields(list[str]):
                The names of the key columns.
            sensitive_fields(list[str]):
                The names of the sensitive columns.
        """
        key_table = allow_nan(synthetic_data[key_fields])
        sensitive_table = allow_nan(synthetic_data[sensitive_fields])
        self.key_processor.fit(key_table)
        self.sensitive_processor.fit(sensitive_table)

        key_train = self.key_processor.transform(key_table)
        sensitive_train = self.sensitive_processor.transform(sensitive_table)
        self.predictor.fit(key_train, sensitive_train)

    def predict(self, key_data):
        """Make a prediction of the sensitive data given keys.

        Args:
            key_data(tuple):
                The key data.

        Returns:
            tuple:
                The predicted sensitive data.
        """
        keys = allow_nan_array(key_data)  # de-nan key attributes
        try:
            # key attributes in ML ready format
            keys_transform = self.key_processor.transform([keys])
        except ValueError:  # Some attributes of the input haven't appeared in synthetic tables
            return None

        sensitive_pred = self.predictor.predict(keys_transform)
        if len(np.array(sensitive_pred).shape) == 1:
            sensitive_pred = [sensitive_pred]

        # predicted sensitive attributes in original format
        sensitives = self.sensitive_processor.inverse_transform(sensitive_pred)
        return tuple(sensitives[0])

class SVCWrapper():
    """A wrapper arround `sklearn.svm.SVC` to support multidimensional y."""

    def __init__(self):
        self.predictors = []

    def fit(self, X, Y):
        """Fit the classifier to training data X and lables Y.

        Arguments:
            X (np.array):
                training data matrix of shape (n_samples, n_features)
            Y (np.array):
                label matrix of shape (n_samples, n_labels)
        """
        n_labels = Y.shape[1]
        for idx in range(n_labels):
            y = Y[:, idx]
            predictor = SVC()
            predictor.fit(X, y)
            self.predictors.append(predictor)

    def predict(self, X):
        """Predict the labels corresponding to data X.

        Arguments:
            X (np.array):
                training data matrix of shape (n_samples, n_features)

        Returns:
            np.array: label matrix of shape (n_samples, n_labels)
        """
        Y = []
        for predictor in self.predictors:
            Y.append(predictor.predict(X))

        Y = np.array(Y).T
        return Y

class NBWrapper():
    """A wrapper arround `sklearn.naive_bayes.CategoricalNB` to support multidimensional y."""

    def __init__(self):
        self.predictors = []

    def fit(self, X, Y):
        """Fit the classifier to training data X and lables Y.

        Arguments:
            X (np.array):
                training data matrix of shape (n_samples, n_features)
            Y (np.array):
                label matrix of shape (n_samples, n_labels)
        """
        n_labels = Y.shape[1]
        for idx in range(n_labels):
            y = Y[:, idx]
            predictor = sklearn.naive_bayes.CategoricalNB()
            predictor.fit(X, y)
            self.predictors.append(predictor)

    def predict(self, X):
        """Predict the labels corresponding to data X.

        Arguments:
            X (np.array): training data matrix of shape (n_samples, n_features)

        Returns:
            np.array: label matrix of shape (n_samples, n_labels)
        """
        Y = []
        for predictor in self.predictors:
            Y.append(predictor.predict(X))
        Y = np.array(Y).T
        return Y

class CategoricalNBAttacker(CategoricalSklearnAttacker):
    """The Categorical NaiveBaysian privacy attaker.

    Uses a naive bayesian classifier to calculate the score based on prediction accuracy.
    """

    KEY_TYPE = CategoricalType.CLASS_NUM
    SENSITIVE_TYPE = CategoricalType.CLASS_NUM
    SKL_LEARNER = NBWrapper

class CategoricalNB(CategoricalPrivacyMetric):
    """The Categorical NaiveBaysian privacy metric. Scored based on the CategoricalNBAttacker."""

    name = 'Categorical NaiveBayesian'
    MODEL = CategoricalNBAttacker
    ACCURACY_BASE = True

class CategoricalKNNAttacker(CategoricalSklearnAttacker):
    """The Categorical KNN (k nearest neighbors) privacy attaker.

    Uses a KNN classifier to calculate the score based on prediction accuracy.
    """

    KEY_TYPE = CategoricalType.ONE_HOT
    SENSITIVE_TYPE = CategoricalType.CLASS_NUM
    SKL_LEARNER = KNeighborsClassifier

class CategoricalKNN(CategoricalPrivacyMetric):
    """The Categorical KNN privacy metric. Scored based on the KNNAttacker."""

    name = 'K-Nearest Neighbors'
    MODEL = CategoricalKNNAttacker
    ACCURACY_BASE = True

class CategoricalRFAttacker(CategoricalSklearnAttacker):
    """The Categorical RF (Random Forest) privacy attaker.

    Uses a RF classifier to calculate the score based on prediction accuracy.
    """

    KEY_TYPE = CategoricalType.ONE_HOT
    SENSITIVE_TYPE = CategoricalType.CLASS_NUM
    SKL_LEARNER = RandomForestClassifier

class CategoricalRF(CategoricalPrivacyMetric):
    """The Categorical RF privacy metric. Scored based on the CategoricalRFAttacker."""

    name = 'Categorical Random Forest'
    MODEL = CategoricalRFAttacker
    ACCURACY_BASE = True

class CategoricalSVMAttacker(CategoricalSklearnAttacker):
    """The Categorical SVM (Support Vector Machine) privacy attaker.

    Uses a SVM classifier to calculate the score based on prediction accuracy.
    """

    KEY_TYPE = CategoricalType.ONE_HOT
    SENSITIVE_TYPE = CategoricalType.CLASS_NUM
    SKL_LEARNER = SVCWrapper

class CategoricalSVM(CategoricalPrivacyMetric):
    """The Categorical SVM privacy metric. Scored based on the CategoricalSVMAttacker."""

    name = 'Support Vector Classifier'
    MODEL = CategoricalSVMAttacker
    ACCURACY_BASE = True

max_stars_repo_name: TanguyUrvoy/SDMetrics

max_issues_repo_name: TanguyUrvoy/SDMetrics

max_forks_repo_name: TanguyUrvoy/SDMetrics