# coding: utf-8
# Copyright (c) Pymatgen Development Team.
# Distributed under the terms of the MIT License.

"""
This module contains the object used to describe the possible bonded atoms based on a Voronoi analysis.
"""

__author__ = "David Waroquiers"
__copyright__ = "Copyright 2012, The Materials Project"
__credits__ = "Geoffroy Hautier"
__version__ = "2.0"
__maintainer__ = "David Waroquiers"
__email__ = "david.waroquiers@gmail.com"
__date__ = "Feb 20, 2016"

import logging
import time

import numpy as np
from monty.json import MSONable
from scipy.spatial import Voronoi

from pymatgen.analysis.chemenv.utils.coordination_geometry_utils import (
    get_lower_and_upper_f,
    my_solid_angle,
    rectangle_surface_intersection,
)
from pymatgen.analysis.chemenv.utils.defs_utils import AdditionalConditions
from pymatgen.analysis.chemenv.utils.math_utils import normal_cdf_step
from pymatgen.core.sites import PeriodicSite
from pymatgen.core.structure import Structure

def from_bson_voronoi_list2(bson_nb_voro_list2, structure):
    """
    Returns the voronoi_list needed for the VoronoiContainer object from a bson-encoded voronoi_list.

    Args:
        bson_nb_voro_list2: List of periodic sites involved in the Voronoi.
        structure: Structure object.

    Returns:
        The voronoi_list needed for the VoronoiContainer (with PeriodicSites as keys of the dictionary - not
        allowed in the BSON format).
    """
    voronoi_list = [None] * len(bson_nb_voro_list2)
    for isite, voro in enumerate(bson_nb_voro_list2):
        if voro is None or voro == "None":
            continue
        voronoi_list[isite] = []
        for psd, dd in voro:
            struct_site = structure[dd["index"]]
            periodic_site = PeriodicSite(
                struct_site._species,
                struct_site.frac_coords + psd[1],
                struct_site._lattice,
                properties=struct_site.properties,
            )
            dd["site"] = periodic_site
            voronoi_list[isite].append(dd)
    return voronoi_list

class DetailedVoronoiContainer(MSONable):
    """
    Class used to store the full Voronoi of a given structure.
    """

    AC = AdditionalConditions()
    default_voronoi_cutoff = 10.0
    default_normalized_distance_tolerance = 1e-5
    default_normalized_angle_tolerance = 1e-3

    def __init__(
        self,
        structure=None,
        voronoi_list2=None,
        voronoi_cutoff=default_voronoi_cutoff,
        isites=None,
        normalized_distance_tolerance=default_normalized_distance_tolerance,
        normalized_angle_tolerance=default_normalized_angle_tolerance,
        additional_conditions=None,
        valences=None,
        maximum_distance_factor=None,
        minimum_angle_factor=None,
    ):
        """
        Constructor for the VoronoiContainer object. Either a structure is given, in which case the Voronoi is
        computed, or the different components of the VoronoiContainer are given (used in the from_dict method).

        Args:
            structure: Structure for which the Voronoi is computed.
            voronoi_list2: List of voronoi polyhedrons for each site.
            voronoi_cutoff: cutoff used for the voronoi.
            isites: indices of sites for which the Voronoi has to be computed.
            normalized_distance_tolerance: Tolerance for two normalized distances to be considered equal.
            normalized_angle_tolerance:Tolerance for two normalized angles to be considered equal.
            additional_conditions: Additional conditions to be used.
            valences: Valences of all the sites in the structure (used when additional conditions require it).
            maximum_distance_factor: The maximum distance factor to be considered.
            minimum_angle_factor: The minimum angle factor to be considered.

        Raises:
            RuntimeError if the Voronoi cannot be constructed.
        """
        self.normalized_distance_tolerance = normalized_distance_tolerance
        self.normalized_angle_tolerance = normalized_angle_tolerance
        if additional_conditions is None:
            self.additional_conditions = [self.AC.NONE, self.AC.ONLY_ACB]
        else:
            self.additional_conditions = additional_conditions
        self.valences = valences
        self.maximum_distance_factor = maximum_distance_factor
        self.minimum_angle_factor = minimum_angle_factor
        if isites is None:
            indices = list(range(len(structure)))
        else:
            indices = isites
        self.structure = structure
        logging.debug("Setting Voronoi list")
        if voronoi_list2 is not None:
            self.voronoi_list2 = voronoi_list2
        else:
            self.setup_voronoi_list(indices=indices, voronoi_cutoff=voronoi_cutoff)
        logging.debug("Setting neighbors distances and angles")
        t1 = time.process_time()
        self.setup_neighbors_distances_and_angles(indices=indices)
        t2 = time.process_time()
        logging.debug("Neighbors distances and angles set up in {:.2f} seconds".format(t2 - t1))

    def setup_voronoi_list(self, indices, voronoi_cutoff):
        """
        Set up of the voronoi list of neighbours by calling qhull.

        Args:
            indices: indices of the sites for which the Voronoi is needed.
            voronoi_cutoff: Voronoi cutoff for the search of neighbours.

        Raises:
            RuntimeError: If an infinite vertex is found in the voronoi construction.
        """
        self.voronoi_list2 = [None] * len(self.structure)
        self.voronoi_list_coords = [None] * len(self.structure)
        logging.debug("Getting all neighbors in structure")
        struct_neighbors = self.structure.get_all_neighbors(voronoi_cutoff, include_index=True)
        t1 = time.process_time()
        logging.debug("Setting up Voronoi list :")

        for jj, isite in enumerate(indices):
            logging.debug("  - Voronoi analysis for site #{:d} ({:d}/{:d})".format(isite, jj + 1, len(indices)))
            site = self.structure[isite]
            neighbors1 = [(site, 0.0, isite)]
            neighbors1.extend(struct_neighbors[isite])
            distances = [i[1] for i in sorted(neighbors1, key=lambda s: s[1])]
            neighbors = [i[0] for i in sorted(neighbors1, key=lambda s: s[1])]
            qvoronoi_input = [s.coords for s in neighbors]
            voro = Voronoi(points=qvoronoi_input, qhull_options="o Fv")
            all_vertices = voro.vertices

            results2 = []
            maxangle = 0.0
            mindist = 10000.0
            for iridge, ridge_points in enumerate(voro.ridge_points):
                if 0 in ridge_points:
                    ridge_vertices_indices = voro.ridge_vertices[iridge]
                    if -1 in ridge_vertices_indices:
                        raise RuntimeError(
                            "This structure is pathological," " infinite vertex in the voronoi " "construction"
                        )

                    ridge_point2 = max(ridge_points)
                    facets = [all_vertices[i] for i in ridge_vertices_indices]
                    sa = my_solid_angle(site.coords, facets)
                    maxangle = max([sa, maxangle])

                    mindist = min([mindist, distances[ridge_point2]])
                    for iii, sss in enumerate(self.structure):
                        if neighbors[ridge_point2].is_periodic_image(sss, tolerance=1.0e-6):
                            myindex = iii
                            break
                    results2.append(
                        {
                            "site": neighbors[ridge_point2],
                            "angle": sa,
                            "distance": distances[ridge_point2],
                            "index": myindex,
                        }
                    )
            for dd in results2:
                dd["normalized_angle"] = dd["angle"] / maxangle
                dd["normalized_distance"] = dd["distance"] / mindist
            self.voronoi_list2[isite] = results2
            self.voronoi_list_coords[isite] = np.array([dd["site"].coords for dd in results2])
        t2 = time.process_time()
        logging.debug("Voronoi list set up in {:.2f} seconds".format(t2 - t1))

    def setup_neighbors_distances_and_angles(self, indices):
        """
        Initializes the angle and distance separations.

        Args:
            indices: Indices of the sites for which the Voronoi is needed.
        """
        self.neighbors_distances = [None] * len(self.structure)
        self.neighbors_normalized_distances = [None] * len(self.structure)
        self.neighbors_angles = [None] * len(self.structure)
        self.neighbors_normalized_angles = [None] * len(self.structure)
        for isite in indices:
            results = self.voronoi_list2[isite]
            if results is None:
                continue
            # Initializes neighbors distances and normalized distances groups
            self.neighbors_distances[isite] = []
            self.neighbors_normalized_distances[isite] = []
            normalized_distances = [nb_dict["normalized_distance"] for nb_dict in results]
            isorted_distances = np.argsort(normalized_distances)
            self.neighbors_normalized_distances[isite].append(
                {
                    "min": normalized_distances[isorted_distances[0]],
                    "max": normalized_distances[isorted_distances[0]],
                }
            )
            self.neighbors_distances[isite].append(
                {
                    "min": results[isorted_distances[0]]["distance"],
                    "max": results[isorted_distances[0]]["distance"],
                }
            )
            icurrent = 0
            nb_indices = {int(isorted_distances[0])}
            dnb_indices = {int(isorted_distances[0])}
            for idist in iter(isorted_distances):
                wd = normalized_distances[idist]
                if self.maximum_distance_factor is not None:
                    if wd > self.maximum_distance_factor:
                        self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                        self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                        self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                        self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                        break
                if np.isclose(
                    wd,
                    self.neighbors_normalized_distances[isite][icurrent]["max"],
                    rtol=0.0,
                    atol=self.normalized_distance_tolerance,
                ):
                    self.neighbors_normalized_distances[isite][icurrent]["max"] = wd
                    self.neighbors_distances[isite][icurrent]["max"] = results[idist]["distance"]
                    dnb_indices.add(int(idist))
                else:
                    self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                    self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                    self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                    self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                    dnb_indices = {int(idist)}
                    self.neighbors_normalized_distances[isite].append({"min": wd, "max": wd})
                    self.neighbors_distances[isite].append(
                        {
                            "min": results[idist]["distance"],
                            "max": results[idist]["distance"],
                        }
                    )
                    icurrent += 1
                nb_indices.add(int(idist))
            else:
                self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
            for idist in range(len(self.neighbors_distances[isite]) - 1):
                dist_dict = self.neighbors_distances[isite][idist]
                dist_dict_next = self.neighbors_distances[isite][idist + 1]
                dist_dict["next"] = dist_dict_next["min"]
                ndist_dict = self.neighbors_normalized_distances[isite][idist]
                ndist_dict_next = self.neighbors_normalized_distances[isite][idist + 1]
                ndist_dict["next"] = ndist_dict_next["min"]
            if self.maximum_distance_factor is not None:
                dfact = self.maximum_distance_factor
            else:
                dfact = self.default_voronoi_cutoff / self.neighbors_distances[isite][0]["min"]
            self.neighbors_normalized_distances[isite][-1]["next"] = dfact
            self.neighbors_distances[isite][-1]["next"] = dfact * self.neighbors_distances[isite][0]["min"]
            # Initializes neighbors angles and normalized angles groups
            self.neighbors_angles[isite] = []
            self.neighbors_normalized_angles[isite] = []
            normalized_angles = [nb_dict["normalized_angle"] for nb_dict in results]
            isorted_angles = np.argsort(normalized_angles)[::-1]
            self.neighbors_normalized_angles[isite].append(
                {
                    "max": normalized_angles[isorted_angles[0]],
                    "min": normalized_angles[isorted_angles[0]],
                }
            )
            self.neighbors_angles[isite].append(
                {
                    "max": results[isorted_angles[0]]["angle"],
                    "min": results[isorted_angles[0]]["angle"],
                }
            )
            icurrent = 0
            nb_indices = {int(isorted_angles[0])}
            dnb_indices = {int(isorted_angles[0])}
            for iang in iter(isorted_angles):
                wa = normalized_angles[iang]
                if self.minimum_angle_factor is not None:
                    if wa < self.minimum_angle_factor:
                        self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                        self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                        self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                        self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                        break
                if np.isclose(
                    wa,
                    self.neighbors_normalized_angles[isite][icurrent]["min"],
                    rtol=0.0,
                    atol=self.normalized_angle_tolerance,
                ):
                    self.neighbors_normalized_angles[isite][icurrent]["min"] = wa
                    self.neighbors_angles[isite][icurrent]["min"] = results[iang]["angle"]
                    dnb_indices.add(int(iang))
                else:
                    self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                    self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                    self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                    self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                    dnb_indices = {int(iang)}
                    self.neighbors_normalized_angles[isite].append({"max": wa, "min": wa})
                    self.neighbors_angles[isite].append({"max": results[iang]["angle"], "min": results[iang]["angle"]})
                    icurrent += 1
                nb_indices.add(int(iang))
            else:
                self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
            for iang in range(len(self.neighbors_angles[isite]) - 1):
                ang_dict = self.neighbors_angles[isite][iang]
                ang_dict_next = self.neighbors_angles[isite][iang + 1]
                ang_dict["next"] = ang_dict_next["max"]
                nang_dict = self.neighbors_normalized_angles[isite][iang]
                nang_dict_next = self.neighbors_normalized_angles[isite][iang + 1]
                nang_dict["next"] = nang_dict_next["max"]
            if self.minimum_angle_factor is not None:
                afact = self.minimum_angle_factor
            else:
                afact = 0.0
            self.neighbors_normalized_angles[isite][-1]["next"] = afact
            self.neighbors_angles[isite][-1]["next"] = afact * self.neighbors_angles[isite][0]["max"]

    def _precompute_additional_conditions(self, ivoronoi, voronoi, valences):
        additional_conditions = {ac: [] for ac in self.additional_conditions}
        for ips, (ps, vals) in enumerate(voronoi):
            for ac in self.additional_conditions:
                additional_conditions[ac].append(
                    self.AC.check_condition(
                        condition=ac,
                        structure=self.structure,
                        parameters={
                            "valences": valences,
                            "neighbor_index": vals["index"],
                            "site_index": ivoronoi,
                        },
                    )
                )
        return additional_conditions

    def _precompute_distance_conditions(self, ivoronoi, voronoi):
        distance_conditions = []
        for idp, dp_dict in enumerate(self.neighbors_normalized_distances[ivoronoi]):
            distance_conditions.append([])
            dp = dp_dict["max"]
            for ips, (ps, vals) in enumerate(voronoi):
                distance_conditions[idp].append(
                    vals["normalized_distance"] <= dp
                    or np.isclose(
                        vals["normalized_distance"],
                        dp,
                        rtol=0.0,
                        atol=self.normalized_distance_tolerance / 2.0,
                    )
                )
        return distance_conditions

    def _precompute_angle_conditions(self, ivoronoi, voronoi):
        angle_conditions = []
        for iap, ap_dict in enumerate(self.neighbors_normalized_angles[ivoronoi]):
            angle_conditions.append([])
            ap = ap_dict["max"]
            for ips, (ps, vals) in enumerate(voronoi):
                angle_conditions[iap].append(
                    vals["normalized_angle"] >= ap
                    or np.isclose(
                        vals["normalized_angle"],
                        ap,
                        rtol=0.0,
                        atol=self.normalized_angle_tolerance / 2.0,
                    )
                )
        return angle_conditions

    # def neighbors_map(self, isite, distfactor, angfactor, additional_condition):
    #     if self.neighbors_normalized_distances[isite] is None:
    #         return None
    #     dist_where = np.argwhere(
    #         np.array([wd['min'] for wd in self.neighbors_normalized_distances[isite]]) <= distfactor)
    #     if len(dist_where) == 0:
    #         return None
    #     idist = dist_where[-1][0]
    #     ang_where = np.argwhere(np.array([wa['max'] for wa in self.neighbors_normalized_angles[isite]]) >= angfactor)
    #     if len(ang_where) == 0:
    #         return None
    #     iang = ang_where[0][0]
    #     if self.additional_conditions.count(additional_condition) != 1:
    #         return None
    #     i_additional_condition = self.additional_conditions.index(additional_condition)
    #     return {'i_distfactor': idist, 'i_angfactor': iang, 'i_additional_condition': i_additional_condition}

    def neighbors_surfaces(self, isite, surface_calculation_type=None, max_dist=2.0):
        """
        Get the different surfaces corresponding to the different distance-angle cutoffs for a given site.

        Args:
            isite: Index of the site
            surface_calculation_type: How to compute the surface.
            max_dist: The maximum distance factor to be considered.

        Returns:
            Surfaces for each distance-angle cutoff.
        """
        if self.voronoi_list2[isite] is None:
            return None
        bounds_and_limits = self.voronoi_parameters_bounds_and_limits(isite, surface_calculation_type, max_dist)
        distance_bounds = bounds_and_limits["distance_bounds"]
        angle_bounds = bounds_and_limits["angle_bounds"]
        surfaces = np.zeros((len(distance_bounds), len(angle_bounds)), np.float_)
        for idp in range(len(distance_bounds) - 1):
            this_dist_plateau = distance_bounds[idp + 1] - distance_bounds[idp]
            for iap in range(len(angle_bounds) - 1):
                this_ang_plateau = angle_bounds[iap + 1] - angle_bounds[iap]
                surfaces[idp][iap] = np.absolute(this_dist_plateau * this_ang_plateau)
        return surfaces

    def neighbors_surfaces_bounded(self, isite, surface_calculation_options=None):
        """
        Get the different surfaces (using boundaries) corresponding to the different distance-angle cutoffs
        for a given site.

        Args:
            isite: Index of the site.
            surface_calculation_options: Options for the boundaries.

        Returns:
            Surfaces for each distance-angle cutoff.
        """
        if self.voronoi_list2[isite] is None:
            return None
        if surface_calculation_options is None:
            surface_calculation_options = {
                "type": "standard_elliptic",
                "distance_bounds": {"lower": 1.2, "upper": 1.8},
                "angle_bounds": {"lower": 0.1, "upper": 0.8},
            }
        if surface_calculation_options["type"] in [
            "standard_elliptic",
            "standard_diamond",
            "standard_spline",
        ]:
            plot_type = {
                "distance_parameter": ("initial_normalized", None),
                "angle_parameter": ("initial_normalized", None),
            }
        else:
            raise ValueError(
                'Type "{}" for the surface calculation in DetailedVoronoiContainer '
                "is invalid".format(surface_calculation_options["type"])
            )
        max_dist = surface_calculation_options["distance_bounds"]["upper"] + 0.1
        bounds_and_limits = self.voronoi_parameters_bounds_and_limits(
            isite=isite, plot_type=plot_type, max_dist=max_dist
        )

        distance_bounds = bounds_and_limits["distance_bounds"]
        angle_bounds = bounds_and_limits["angle_bounds"]
        lower_and_upper_functions = get_lower_and_upper_f(surface_calculation_options=surface_calculation_options)
        mindist = surface_calculation_options["distance_bounds"]["lower"]
        maxdist = surface_calculation_options["distance_bounds"]["upper"]
        minang = surface_calculation_options["angle_bounds"]["lower"]
        maxang = surface_calculation_options["angle_bounds"]["upper"]

        f_lower = lower_and_upper_functions["lower"]
        f_upper = lower_and_upper_functions["upper"]
        surfaces = np.zeros((len(distance_bounds), len(angle_bounds)), np.float_)
        for idp in range(len(distance_bounds) - 1):
            dp1 = distance_bounds[idp]
            dp2 = distance_bounds[idp + 1]
            if dp2 < mindist or dp1 > maxdist:
                continue
            if dp1 < mindist:
                d1 = mindist
            else:
                d1 = dp1
            if dp2 > maxdist:
                d2 = maxdist
            else:
                d2 = dp2
            for iap in range(len(angle_bounds) - 1):
                ap1 = angle_bounds[iap]
                ap2 = angle_bounds[iap + 1]
                if ap1 > ap2:
                    ap1 = angle_bounds[iap + 1]
                    ap2 = angle_bounds[iap]
                if ap2 < minang or ap1 > maxang:
                    continue
                intersection, interror = rectangle_surface_intersection(
                    rectangle=((d1, d2), (ap1, ap2)),
                    f_lower=f_lower,
                    f_upper=f_upper,
                    bounds_lower=[mindist, maxdist],
                    bounds_upper=[mindist, maxdist],
                    check=False,
                )
                surfaces[idp][iap] = intersection
        return surfaces

    @staticmethod
    def _get_vertices_dist_ang_indices(parameter_indices_list):
        pp0 = [pp[0] for pp in parameter_indices_list]
        pp1 = [pp[1] for pp in parameter_indices_list]
        min_idist = min(pp0)
        min_iang = min(pp1)
        max_idist = max(pp0)
        max_iang = max(pp1)
        i_min_angs = np.argwhere(np.array(pp1) == min_iang)
        i_max_dists = np.argwhere(np.array(pp0) == max_idist)
        pp0_at_min_iang = [pp0[ii[0]] for ii in i_min_angs]
        pp1_at_max_idist = [pp1[ii[0]] for ii in i_max_dists]
        max_idist_at_min_iang = max(pp0_at_min_iang)
        min_iang_at_max_idist = min(pp1_at_max_idist)

        p1 = (min_idist, min_iang)
        p2 = (max_idist_at_min_iang, min_iang)
        p3 = (max_idist_at_min_iang, min_iang_at_max_idist)
        p4 = (max_idist, min_iang_at_max_idist)
        p5 = (max_idist, max_iang)
        p6 = (min_idist, max_iang)

        return [p1, p2, p3, p4, p5, p6]

    def maps_and_surfaces(
        self,
        isite,
        surface_calculation_type=None,
        max_dist=2.0,
        additional_conditions=None,
    ):
        """
        Get the different surfaces and their cn_map corresponding to the different distance-angle cutoffs
        for a given site.

        Args:
            isite: Index of the site
            surface_calculation_type: How to compute the surface.
            max_dist: The maximum distance factor to be considered.
            additional_conditions: If additional conditions have to be considered.

        Returns:
            Surfaces and cn_map's for each distance-angle cutoff.
        """
        if self.voronoi_list2[isite] is None:
            return None
        if additional_conditions is None:
            additional_conditions = [self.AC.ONLY_ACB]
        surfaces = self.neighbors_surfaces(
            isite=isite,
            surface_calculation_type=surface_calculation_type,
            max_dist=max_dist,
        )
        maps_and_surfaces = []
        for cn, value in self._unique_coordinated_neighbors_parameters_indices[isite].items():  # pylint: disable=E1101
            for imap, list_parameters_indices in enumerate(value):
                thissurf = 0.0
                for (idp, iap, iacb) in list_parameters_indices:
                    if iacb in additional_conditions:
                        thissurf += surfaces[idp, iap]
                maps_and_surfaces.append(
                    {
                        "map": (cn, imap),
                        "surface": thissurf,
                        "parameters_indices": list_parameters_indices,
                    }
                )
        return maps_and_surfaces

    def maps_and_surfaces_bounded(self, isite, surface_calculation_options=None, additional_conditions=None):
        """
        Get the different surfaces (using boundaries) and their cn_map corresponding to the different
        distance-angle cutoffs for a given site.

        Args:
            isite: Index of the site
            surface_calculation_options: Options for the boundaries.
            additional_conditions: If additional conditions have to be considered.

        Returns:
            Surfaces and cn_map's for each distance-angle cutoff.
        """
        if self.voronoi_list2[isite] is None:
            return None
        if additional_conditions is None:
            additional_conditions = [self.AC.ONLY_ACB]
        surfaces = self.neighbors_surfaces_bounded(isite=isite, surface_calculation_options=surface_calculation_options)
        maps_and_surfaces = []
        for cn, value in self._unique_coordinated_neighbors_parameters_indices[isite].items():  # pylint: disable=E1101
            for imap, list_parameters_indices in enumerate(value):
                thissurf = 0.0
                for (idp, iap, iacb) in list_parameters_indices:
                    if iacb in additional_conditions:
                        thissurf += surfaces[idp, iap]
                maps_and_surfaces.append(
                    {
                        "map": (cn, imap),
                        "surface": thissurf,
                        "parameters_indices": list_parameters_indices,
                    }
                )
        return maps_and_surfaces

    def neighbors(self, isite, distfactor, angfactor, additional_condition=None):
        """
        Get the neighbors of a given site corresponding to a given distance and angle factor.

        Args:
            isite: Index of the site.
            distfactor: Distance factor.
            angfactor: Angle factor.
            additional_condition: Additional condition to be used (currently not implemented).

        Returns:
            List of neighbors of the given site for the given distance and angle factors.
        """
        idist = None
        dfact = None
        for iwd, wd in enumerate(self.neighbors_normalized_distances[isite]):
            if distfactor >= wd["min"]:
                idist = iwd
                dfact = wd["max"]
            else:
                break
        iang = None
        afact = None
        for iwa, wa in enumerate(self.neighbors_normalized_angles[isite]):
            if angfactor <= wa["max"]:
                iang = iwa
                afact = wa["min"]
            else:
                break
        if idist is None or iang is None:
            raise ValueError("Distance or angle parameter not found ...")

        return [
            nb
            for nb in self.voronoi_list2[isite]
            if nb["normalized_distance"] <= dfact and nb["normalized_angle"] >= afact
        ]

    def voronoi_parameters_bounds_and_limits(self, isite, plot_type, max_dist):
        """
        Get the different boundaries and limits of the distance and angle factors for the given site.

        Args:
            isite: Index of the site.
            plot_type: Types of distance/angle parameters to get.
            max_dist: Maximum distance factor.

        Returns:
            Distance and angle bounds and limits.
        """
        # Initializes the distance and angle parameters
        if self.voronoi_list2[isite] is None:
            return None
        if plot_type is None:
            plot_type = {
                "distance_parameter": ("initial_inverse_opposite", None),
                "angle_parameter": ("initial_opposite", None),
            }
        dd = [dist["min"] for dist in self.neighbors_normalized_distances[isite]]
        dd[0] = 1.0
        if plot_type["distance_parameter"][0] == "initial_normalized":
            dd.append(max_dist)
            distance_bounds = np.array(dd)
            dist_limits = [1.0, max_dist]
        elif plot_type["distance_parameter"][0] == "initial_inverse_opposite":
            ddinv = [1.0 / dist for dist in dd]
            ddinv.append(0.0)
            distance_bounds = np.array([1.0 - invdist for invdist in ddinv])
            dist_limits = [0.0, 1.0]
        elif plot_type["distance_parameter"][0] == "initial_inverse3_opposite":
            ddinv = [1.0 / dist ** 3.0 for dist in dd]
            ddinv.append(0.0)
            distance_bounds = np.array([1.0 - invdist for invdist in ddinv])
            dist_limits = [0.0, 1.0]
        else:
            raise NotImplementedError(
                'Plotting type "{}" ' "for the distance is not implemented".format(plot_type["distance_parameter"])
            )
        if plot_type["angle_parameter"][0] == "initial_normalized":
            aa = [0.0]
            aa.extend([ang["max"] for ang in self.neighbors_normalized_angles[isite]])
            angle_bounds = np.array(aa)
        elif plot_type["angle_parameter"][0] == "initial_opposite":
            aa = [0.0]
            aa.extend([ang["max"] for ang in self.neighbors_normalized_angles[isite]])
            aa = [1.0 - ang for ang in aa]
            angle_bounds = np.array(aa)
        else:
            raise NotImplementedError(
                'Plotting type "{}" ' "for the angle is not implemented".format(plot_type["angle_parameter"])
            )
        ang_limits = [0.0, 1.0]
        return {
            "distance_bounds": distance_bounds,
            "distance_limits": dist_limits,
            "angle_bounds": angle_bounds,
            "angle_limits": ang_limits,
        }

    def is_close_to(self, other, rtol=0.0, atol=1e-8):
        """
        Whether two DetailedVoronoiContainer objects are close to each other.

        Args:
            other: Another DetailedVoronoiContainer to be compared with.
            rtol: Relative tolerance to compare values.
            atol: Absolute tolerance to compare values.

        Returns:
            True if the two DetailedVoronoiContainer are close to each other.
        """
        isclose = (
            np.isclose(
                self.normalized_angle_tolerance,
                other.normalized_angle_tolerance,
                rtol=rtol,
                atol=atol,
            )
            and np.isclose(
                self.normalized_distance_tolerance,
                other.normalized_distance_tolerance,
                rtol=rtol,
                atol=atol,
            )
            and self.additional_conditions == other.additional_conditions
            and self.valences == other.valences
        )
        if not isclose:
            return isclose
        for isite, site_voronoi in enumerate(self.voronoi_list2):
            self_to_other_nbs = {}
            for inb, nb in enumerate(site_voronoi):
                if nb is None:
                    if other.voronoi_list2[isite] is None:
                        continue
                    return False
                if other.voronoi_list2[isite] is None:
                    return False
                nb_other = None
                for inb2, nb2 in enumerate(other.voronoi_list2[isite]):
                    if nb["site"] == nb2["site"]:
                        self_to_other_nbs[inb] = inb2
                        nb_other = nb2
                        break
                if nb_other is None:
                    return False
                if not np.isclose(nb["distance"], nb_other["distance"], rtol=rtol, atol=atol):
                    return False
                if not np.isclose(nb["angle"], nb_other["angle"], rtol=rtol, atol=atol):
                    return False
                if not np.isclose(
                    nb["normalized_distance"],
                    nb_other["normalized_distance"],
                    rtol=rtol,
                    atol=atol,
                ):
                    return False
                if not np.isclose(
                    nb["normalized_angle"],
                    nb_other["normalized_angle"],
                    rtol=rtol,
                    atol=atol,
                ):
                    return False
                if nb["index"] != nb_other["index"]:
                    return False
                if nb["site"] != nb_other["site"]:
                    return False
        return True

    def get_rdf_figure(self, isite, normalized=True, figsize=None, step_function=None):
        """
        Get the Radial Distribution Figure for a given site.

        Args:
            isite: Index of the site.
            normalized: Whether to normalize distances.
            figsize: Size of the figure.
            step_function: Type of step function to be used for the RDF.

        Returns:
            Matplotlib figure.
        """

        def dp_func(dp):
            return 1.0 - 1.0 / np.power(dp, 3.0)

        import matplotlib.pyplot as plt

        if step_function is None:
            step_function = {"type": "normal_cdf", "scale": 0.0001}

        # Initializes the figure
        if figsize is None:
            fig = plt.figure()
        else:
            fig = plt.figure(figsize=figsize)
        subplot = fig.add_subplot(111)
        if normalized:
            dists = self.neighbors_normalized_distances[isite]
        else:
            dists = self.neighbors_distances[isite]

        if step_function["type"] == "step_function":
            isorted = np.argsort([dd["min"] for dd in dists])
            sorted_dists = [dists[ii]["min"] for ii in isorted]
            dnb_dists = [len(dists[ii]["dnb_indices"]) for ii in isorted]
            xx = [0.0]
            yy = [0.0]
            for idist, dist in enumerate(sorted_dists):
                xx.append(dist)
                xx.append(dist)
                yy.append(yy[-1])
                yy.append(yy[-1] + dnb_dists[idist])
            xx.append(1.1 * xx[-1])
            yy.append(yy[-1])
        elif step_function["type"] == "normal_cdf":
            scale = step_function["scale"]
            mydists = [dp_func(dd["min"]) for dd in dists]
            mydcns = [len(dd["dnb_indices"]) for dd in dists]
            xx = np.linspace(0.0, 1.1 * max(mydists), num=500)
            yy = np.zeros_like(xx)
            for idist, dist in enumerate(mydists):
                yy += mydcns[idist] * normal_cdf_step(xx, mean=dist, scale=scale)
        else:
            raise ValueError('Step function of type "{}" is not allowed'.format(step_function["type"]))
        subplot.plot(xx, yy)

        return fig

    def get_sadf_figure(self, isite, normalized=True, figsize=None, step_function=None):
        """
        Get the Solid Angle Distribution Figure for a given site.
        Args:
            isite: Index of the site.
            normalized: Whether to normalize angles.
            figsize: Size of the figure.
            step_function: Type of step function to be used for the SADF.

        Returns:
            Matplotlib figure.
        """

        def ap_func(ap):
            return np.power(ap, -0.1)

        import matplotlib.pyplot as plt

        if step_function is None:
            step_function = {"type": "step_function", "scale": 0.0001}

        # Initializes the figure
        if figsize is None:
            fig = plt.figure()
        else:
            fig = plt.figure(figsize=figsize)
        subplot = fig.add_subplot(111)
        if normalized:
            angs = self.neighbors_normalized_angles[isite]
        else:
            angs = self.neighbors_angles[isite]

        if step_function["type"] == "step_function":
            isorted = np.argsort([ap_func(aa["min"]) for aa in angs])
            sorted_angs = [ap_func(angs[ii]["min"]) for ii in isorted]
            dnb_angs = [len(angs[ii]["dnb_indices"]) for ii in isorted]
            xx = [0.0]
            yy = [0.0]
            for iang, ang in enumerate(sorted_angs):
                xx.append(ang)
                xx.append(ang)
                yy.append(yy[-1])
                yy.append(yy[-1] + dnb_angs[iang])
            xx.append(1.1 * xx[-1])
            yy.append(yy[-1])
        elif step_function["type"] == "normal_cdf":
            scale = step_function["scale"]
            myangs = [ap_func(aa["min"]) for aa in angs]
            mydcns = [len(dd["dnb_indices"]) for dd in angs]
            xx = np.linspace(0.0, 1.1 * max(myangs), num=500)
            yy = np.zeros_like(xx)
            for iang, ang in enumerate(myangs):
                yy += mydcns[iang] * normal_cdf_step(xx, mean=ang, scale=scale)
        else:
            raise ValueError('Step function of type "{}" is not allowed'.format(step_function["type"]))
        subplot.plot(xx, yy)

        return fig

    def __eq__(self, other):
        return (
            self.normalized_angle_tolerance == other.normalized_angle_tolerance
            and self.normalized_distance_tolerance == other.normalized_distance_tolerance
            and self.additional_conditions == other.additional_conditions
            and self.valences == other.valences
            and self.voronoi_list2 == other.voronoi_list2
            and self.structure == other.structure
        )

    def __ne__(self, other):
        return not self == other

    def to_bson_voronoi_list2(self):
        """
        Transforms the voronoi_list into a vlist + bson_nb_voro_list, that are BSON-encodable.

        Returns:
            [vlist, bson_nb_voro_list], to be used in the as_dict method.
        """
        bson_nb_voro_list2 = [None] * len(self.voronoi_list2)
        for ivoro, voro in enumerate(self.voronoi_list2):
            if voro is None or voro == "None":
                continue
            site_voro = []
            # {'site': neighbors[nn[1]],
            #  'angle': sa,
            #  'distance': distances[nn[1]],
            #  'index': myindex}
            for nb_dict in voro:
                site = nb_dict["site"]
                site_dict = {key: val for key, val in nb_dict.items() if key not in ["site"]}
                # site_voro.append([ps.as_dict(), dd]) [float(c) for c in self.frac_coords]
                diff = site.frac_coords - self.structure[nb_dict["index"]].frac_coords
                site_voro.append([[nb_dict["index"], [float(c) for c in diff]], site_dict])
            bson_nb_voro_list2[ivoro] = site_voro
        return bson_nb_voro_list2

    def as_dict(self):
        """
        Bson-serializable dict representation of the VoronoiContainer.

        Returns:
            dictionary that is BSON-encodable.
        """
        bson_nb_voro_list2 = self.to_bson_voronoi_list2()
        return {
            "@module": self.__class__.__module__,
            "@class": self.__class__.__name__,
            "bson_nb_voro_list2": bson_nb_voro_list2,
            # "neighbors_lists": self.neighbors_lists,
            "structure": self.structure.as_dict(),
            "normalized_angle_tolerance": self.normalized_angle_tolerance,
            "normalized_distance_tolerance": self.normalized_distance_tolerance,
            "additional_conditions": self.additional_conditions,
            "valences": self.valences,
            "maximum_distance_factor": self.maximum_distance_factor,
            "minimum_angle_factor": self.minimum_angle_factor,
        }

    @classmethod
    def from_dict(cls, d):
        """
        Reconstructs the VoronoiContainer object from a dict representation of the VoronoiContainer created using
        the as_dict method.

        Args:
            d: dict representation of the VoronoiContainer object.

        Returns:
            VoronoiContainer object.
        """
        structure = Structure.from_dict(d["structure"])
        voronoi_list2 = from_bson_voronoi_list2(d["bson_nb_voro_list2"], structure)
        maximum_distance_factor = d["maximum_distance_factor"] if "maximum_distance_factor" in d else None
        minimum_angle_factor = d["minimum_angle_factor"] if "minimum_angle_factor" in d else None
        return cls(
            structure=structure,
            voronoi_list2=voronoi_list2,
            # neighbors_lists=neighbors_lists,
            normalized_angle_tolerance=d["normalized_angle_tolerance"],
            normalized_distance_tolerance=d["normalized_distance_tolerance"],
            additional_conditions=d["additional_conditions"],
            valences=d["valences"],
            maximum_distance_factor=maximum_distance_factor,
            minimum_angle_factor=minimum_angle_factor,
        )


# Authors: Alexandre Gramfort <alexandre.gramfort@inria.fr>
# License: BSD 3 clause

import numpy as np
from scipy import sparse

from sklearn.utils.testing import assert_equal
from sklearn.utils.testing import assert_array_equal
from sklearn.utils.testing import assert_raises

from sklearn.linear_model.randomized_l1 import (lasso_stability_path,
                                                RandomizedLasso,
                                                RandomizedLogisticRegression)
from sklearn.datasets import load_diabetes, load_iris
from sklearn.feature_selection import f_regression, f_classif
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model.base import center_data

diabetes = load_diabetes()
X = diabetes.data
y = diabetes.target
X = StandardScaler().fit_transform(X)
X = X[:, [2, 3, 6, 7, 8]]

# test that the feature score of the best features
F, _ = f_regression(X, y)

def test_lasso_stability_path():
    # Check lasso stability path
    # Load diabetes data and add noisy features
    scaling = 0.3
    coef_grid, scores_path = lasso_stability_path(X, y, scaling=scaling,
                                                  random_state=42,
                                                  n_resampling=30)

    assert_array_equal(np.argsort(F)[-3:],
                       np.argsort(np.sum(scores_path, axis=1))[-3:])

def test_randomized_lasso():
    # Check randomized lasso
    scaling = 0.3
    selection_threshold = 0.5

    # or with 1 alpha
    clf = RandomizedLasso(verbose=False, alpha=1, random_state=42,
                          scaling=scaling,
                          selection_threshold=selection_threshold)
    feature_scores = clf.fit(X, y).scores_
    assert_array_equal(np.argsort(F)[-3:], np.argsort(feature_scores)[-3:])

    # or with many alphas
    clf = RandomizedLasso(verbose=False, alpha=[1, 0.8], random_state=42,
                          scaling=scaling,
                          selection_threshold=selection_threshold)
    feature_scores = clf.fit(X, y).scores_
    assert_equal(clf.all_scores_.shape, (X.shape[1], 2))
    assert_array_equal(np.argsort(F)[-3:], np.argsort(feature_scores)[-3:])

    X_r = clf.transform(X)
    X_full = clf.inverse_transform(X_r)
    assert_equal(X_r.shape[1], np.sum(feature_scores > selection_threshold))
    assert_equal(X_full.shape, X.shape)

    clf = RandomizedLasso(verbose=False, alpha='aic', random_state=42,
                          scaling=scaling)
    feature_scores = clf.fit(X, y).scores_
    assert_array_equal(feature_scores, X.shape[1] * [1.])

    clf = RandomizedLasso(verbose=False, scaling=-0.1)
    assert_raises(ValueError, clf.fit, X, y)

    clf = RandomizedLasso(verbose=False, scaling=1.1)
    assert_raises(ValueError, clf.fit, X, y)

def test_randomized_logistic():
    # Check randomized sparse logistic regression
    iris = load_iris()
    X = iris.data[:, [0, 2]]
    y = iris.target
    X = X[y != 2]
    y = y[y != 2]

    F, _ = f_classif(X, y)

    scaling = 0.3
    clf = RandomizedLogisticRegression(verbose=False, C=1., random_state=42,
                                       scaling=scaling, n_resampling=50,
                                       tol=1e-3)
    X_orig = X.copy()
    feature_scores = clf.fit(X, y).scores_
    assert_array_equal(X, X_orig)   # fit does not modify X
    assert_array_equal(np.argsort(F), np.argsort(feature_scores))

    clf = RandomizedLogisticRegression(verbose=False, C=[1., 0.5],
                                       random_state=42, scaling=scaling,
                                       n_resampling=50, tol=1e-3)
    feature_scores = clf.fit(X, y).scores_
    assert_array_equal(np.argsort(F), np.argsort(feature_scores))

def test_randomized_logistic_sparse():
    # Check randomized sparse logistic regression on sparse data
    iris = load_iris()
    X = iris.data[:, [0, 2]]
    y = iris.target
    X = X[y != 2]
    y = y[y != 2]

    # center here because sparse matrices are usually not centered
    X, y, _, _, _ = center_data(X, y, True, True)

    X_sp = sparse.csr_matrix(X)

    F, _ = f_classif(X, y)

    scaling = 0.3
    clf = RandomizedLogisticRegression(verbose=False, C=1., random_state=42,
                                       scaling=scaling, n_resampling=50,
                                       tol=1e-3)
    feature_scores = clf.fit(X, y).scores_
    clf = RandomizedLogisticRegression(verbose=False, C=1., random_state=42,
                                       scaling=scaling, n_resampling=50,
                                       tol=1e-3)
    feature_scores_sp = clf.fit(X_sp, y).scores_
    assert_array_equal(feature_scores, feature_scores_sp)


# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Debugging script for checking calculation values."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

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

# import soundfile as sf

def new_data_to_array(fn, datatype='int16'):
  """Converts file information to an in-memory array."""
  vals = []
  with open(fn) as f:
    for n, line in enumerate(f):
      if n != 0:
        vals.extend([int(v, 16) for v in line.split()])
  b = ''.join(map(chr, vals))

  if datatype == 'int8':
    typestr = 'b'
    arraylen = int(len(b))
  elif datatype == 'int16':
    typestr = 'h'
    arraylen = int(len(b) // 2)
  elif datatype == 'int32':
    typestr = 'i'
    arraylen = int(len(b) // 4)
  if datatype == 'uint8':
    typestr = 'B'
    arraylen = int(len(b))
  elif datatype == 'uint16':
    typestr = 'H'
    arraylen = int(len(b) // 2)
  elif datatype == 'uint32':
    typestr = 'I'
    arraylen = int(len(b) // 4)

  y = np.array(struct.unpack('<' + typestr * arraylen, b))

  return y

# x is the fixed-point input in Qm.n format
def to_float(x, n):
  return x.astype(float) * 2**(-n)

micro_windowed_input = new_data_to_array(
    'micro_windowed_input.txt', datatype='int32')
cmsis_windowed_input = new_data_to_array(
    'cmsis_windowed_input.txt', datatype='int16')

micro_dft = new_data_to_array('micro_dft.txt', datatype='int32')
cmsis_dft = new_data_to_array('cmsis_dft.txt', datatype='int16')
py_dft = np.fft.rfft(to_float(cmsis_windowed_input, 15), n=512)
py_result = np.empty((2 * py_dft.size), dtype=np.float)
py_result[0::2] = np.real(py_dft)
py_result[1::2] = np.imag(py_dft)

micro_power = new_data_to_array('micro_power.txt', datatype='int32')
cmsis_power = new_data_to_array('cmsis_power.txt', datatype='int16')
py_power = np.square(np.abs(py_dft))

micro_power_avg = new_data_to_array('micro_power_avg.txt', datatype='uint8')
cmsis_power_avg = new_data_to_array('cmsis_power_avg.txt', datatype='uint8')

plt.figure(1)
plt.subplot(311)
plt.plot(micro_windowed_input, label='Micro fixed')
plt.legend()
plt.subplot(312)
plt.plot(cmsis_windowed_input, label='CMSIS fixed')
plt.legend()
plt.subplot(313)
plt.plot(to_float(micro_windowed_input, 30), label='Micro to float')
plt.plot(to_float(cmsis_windowed_input, 15), label='CMSIS to float')
plt.legend()

plt.figure(2)
plt.subplot(311)
plt.plot(micro_dft, label='Micro fixed')
plt.legend()
plt.subplot(312)
plt.plot(cmsis_dft, label='CMSIS fixed')
plt.legend()
plt.subplot(313)
plt.plot(to_float(micro_dft, 22), label='Micro to float')
# CMSIS result has 6 fractionanl bits (not 7) due to documentation error (see
# README.md)
plt.plot(to_float(cmsis_dft, 6), label='CMSIS to float')
plt.plot(py_result, label='Python result')
plt.legend()

plt.figure(3)
plt.subplot(311)
plt.plot(micro_power, label='Micro fixed')
plt.legend()
plt.subplot(312)
plt.plot(cmsis_power[0:256], label='CMSIS fixed')
plt.legend()
plt.subplot(313)
plt.plot(to_float(micro_power, 22), label='Micro to float')
plt.plot(to_float(cmsis_power[0:256], 6), label='CMSIS to float')
plt.plot(py_power, label='Python result')
plt.legend()

plt.figure(4)
plt.plot(micro_power_avg, label='Micro fixed')
plt.plot(cmsis_power_avg, label='CMSIS fixed')
plt.legend()
plt.show()

# t = np.arange(16000.*0.03)/16000.
# # Factor of 10 because micro preprocessing overflows otherwise
# sin1k = 0.1*np.sin(2*np.pi*1000*t)
#
# plt.figure(1)
# plt.subplot(511)
# plt.plot(sin1k)
# plt.title('Input sine')
#
# plt.subplot(512)
# plt.plot(to_float(micro_windowed_input, 30), label='Micro-Lite')
# plt.plot(to_float(cmsis_windowed_input, 15), label='CMSIS')
# plt.title('Windowed sine')
# plt.legend(loc='center right')
#
# plt.subplot(513)
# plt.plot(to_float(micro_dft, 22), label='Micro-Lite')
# plt.plot(to_float(cmsis_dft, 6), label='CMSIS')
# plt.title('FFT')
# plt.legend(loc='center')
#
# plt.subplot(514)
# plt.plot(to_float(micro_power, 22), label='Micro-Lite')
# plt.plot(to_float(cmsis_power[0:256], 6), label='CMSIS')
# plt.title('|FFT|^2')
# plt.legend(loc='center right')
#
# plt.subplot(515)
# plt.plot(micro_power_avg, label='Micro-Lite')
# plt.plot(cmsis_power_avg, label='CMSIS')
# plt.title('Averaged |FFT|^2')
# plt.legend(loc='center right')
#
# plt.tight_layout(pad=0, w_pad=0.2, h_pad=0.2)
#
# plt.show()
#


"""
Testing for Gaussian Process module (sklearn.gaussian_process)
"""

# Author: Vincent Dubourg <vincent.dubourg@gmail.com>
# Licence: BSD 3 clause

from nose.tools import raises
from nose.tools import assert_true

import numpy as np

from sklearn.gaussian_process import GaussianProcess
from sklearn.gaussian_process import regression_models as regression
from sklearn.gaussian_process import correlation_models as correlation
from sklearn.datasets import make_regression
from sklearn.utils.testing import assert_greater

f = lambda x: x * np.sin(x)
X = np.atleast_2d([1., 3., 5., 6., 7., 8.]).T
X2 = np.atleast_2d([2., 4., 5.5, 6.5, 7.5]).T
y = f(X).ravel()

def test_1d(regr=regression.constant, corr=correlation.squared_exponential,
            random_start=10, beta0=None):
    # MLE estimation of a one-dimensional Gaussian Process model.
    # Check random start optimization.
    # Test the interpolating property.
    gp = GaussianProcess(regr=regr, corr=corr, beta0=beta0,
                         theta0=1e-2, thetaL=1e-4, thetaU=1e-1,
                         random_start=random_start, verbose=False).fit(X, y)
    y_pred, MSE = gp.predict(X, eval_MSE=True)
    y2_pred, MSE2 = gp.predict(X2, eval_MSE=True)

    assert_true(np.allclose(y_pred, y) and np.allclose(MSE, 0.)
                and np.allclose(MSE2, 0., atol=10))

def test_2d(regr=regression.constant, corr=correlation.squared_exponential,
            random_start=10, beta0=None):
    # MLE estimation of a two-dimensional Gaussian Process model accounting for
    # anisotropy. Check random start optimization.
    # Test the interpolating property.
    b, kappa, e = 5., .5, .1
    g = lambda x: b - x[:, 1] - kappa * (x[:, 0] - e) ** 2.
    X = np.array([[-4.61611719, -6.00099547],
                  [4.10469096, 5.32782448],
                  [0.00000000, -0.50000000],
                  [-6.17289014, -4.6984743],
                  [1.3109306, -6.93271427],
                  [-5.03823144, 3.10584743],
                  [-2.87600388, 6.74310541],
                  [5.21301203, 4.26386883]])
    y = g(X).ravel()

    thetaL = [1e-4] * 2
    thetaU = [1e-1] * 2
    gp = GaussianProcess(regr=regr, corr=corr, beta0=beta0,
                         theta0=[1e-2] * 2, thetaL=thetaL,
                         thetaU=thetaU,
                         random_start=random_start, verbose=False)
    gp.fit(X, y)
    y_pred, MSE = gp.predict(X, eval_MSE=True)

    assert_true(np.allclose(y_pred, y) and np.allclose(MSE, 0.))

    eps = np.finfo(gp.theta_.dtype).eps
    assert_true(np.all(gp.theta_ >= thetaL - eps))  # Lower bounds of hyperparameters
    assert_true(np.all(gp.theta_ <= thetaU + eps))  # Upper bounds of hyperparameters

def test_2d_2d(regr=regression.constant, corr=correlation.squared_exponential,
               random_start=10, beta0=None):
    # MLE estimation of a two-dimensional Gaussian Process model accounting for
    # anisotropy. Check random start optimization.
    # Test the GP interpolation for 2D output
    b, kappa, e = 5., .5, .1
    g = lambda x: b - x[:, 1] - kappa * (x[:, 0] - e) ** 2.
    f = lambda x: np.vstack((g(x), g(x))).T
    X = np.array([[-4.61611719, -6.00099547],
                  [4.10469096, 5.32782448],
                  [0.00000000, -0.50000000],
                  [-6.17289014, -4.6984743],
                  [1.3109306, -6.93271427],
                  [-5.03823144, 3.10584743],
                  [-2.87600388, 6.74310541],
                  [5.21301203, 4.26386883]])
    y = f(X)
    gp = GaussianProcess(regr=regr, corr=corr, beta0=beta0,
                         theta0=[1e-2] * 2, thetaL=[1e-4] * 2,
                         thetaU=[1e-1] * 2,
                         random_start=random_start, verbose=False)
    gp.fit(X, y)
    y_pred, MSE = gp.predict(X, eval_MSE=True)

    assert_true(np.allclose(y_pred, y) and np.allclose(MSE, 0.))

@raises(ValueError)
def test_wrong_number_of_outputs():
    gp = GaussianProcess()
    gp.fit([[1, 2, 3], [4, 5, 6]], [1, 2, 3])

def test_more_builtin_correlation_models(random_start=1):
    # Repeat test_1d and test_2d for several built-in correlation
    # models specified as strings.
    all_corr = ['absolute_exponential', 'squared_exponential', 'cubic',
                'linear']

    for corr in all_corr:
        test_1d(regr='constant', corr=corr, random_start=random_start)
        test_2d(regr='constant', corr=corr, random_start=random_start)
        test_2d_2d(regr='constant', corr=corr, random_start=random_start)

def test_ordinary_kriging():
    # Repeat test_1d and test_2d with given regression weights (beta0) for
    # different regression models (Ordinary Kriging).
    test_1d(regr='linear', beta0=[0., 0.5])
    test_1d(regr='quadratic', beta0=[0., 0.5, 0.5])
    test_2d(regr='linear', beta0=[0., 0.5, 0.5])
    test_2d(regr='quadratic', beta0=[0., 0.5, 0.5, 0.5, 0.5, 0.5])
    test_2d_2d(regr='linear', beta0=[0., 0.5, 0.5])
    test_2d_2d(regr='quadratic', beta0=[0., 0.5, 0.5, 0.5, 0.5, 0.5])

def test_no_normalize():
    gp = GaussianProcess(normalize=False).fit(X, y)
    y_pred = gp.predict(X)
    assert_true(np.allclose(y_pred, y))

def test_random_starts():
    # Test that an increasing number of random-starts of GP fitting only
    # increases the reduced likelihood function of the optimal theta.
    n_samples, n_features = 50, 3
    np.random.seed(0)
    rng = np.random.RandomState(0)
    X = rng.randn(n_samples, n_features) * 2 - 1
    y = np.sin(X).sum(axis=1) + np.sin(3 * X).sum(axis=1)
    best_likelihood = -np.inf
    for random_start in range(1, 5):
        gp = GaussianProcess(regr="constant", corr="squared_exponential",
                             theta0=[1e-0] * n_features,
                             thetaL=[1e-4] * n_features,
                             thetaU=[1e+1] * n_features,
                             random_start=random_start, random_state=0,
                             verbose=False).fit(X, y)
        rlf = gp.reduced_likelihood_function()[0]
        assert_greater(rlf, best_likelihood - np.finfo(np.float32).eps)
        best_likelihood = rlf

def test_mse_solving():
    # test the MSE estimate to be sane.
    # non-regression test for ignoring off-diagonals of feature covariance,
    # testing with nugget that renders covariance useless, only
    # using the mean function, with low effective rank of data
    gp = GaussianProcess(corr='absolute_exponential', theta0=1e-4,
                         thetaL=1e-12, thetaU=1e-2, nugget=1e-2,
                         optimizer='Welch', regr="linear", random_state=0)

    X, y = make_regression(n_informative=3, n_features=60, noise=50,
                           random_state=0, effective_rank=1)

    gp.fit(X, y)
    assert_greater(1000, gp.predict(X, eval_MSE=True)[1].mean())


"""
Tools to process TCX files,
specifically for parsing and
converting to other formats.
"""

import numpy as np
import pandas as pd
from lxml import objectify
import dateutil.parser
import logging

TPXNS = "{http://www.garmin.com/xmlschemas/ActivityExtension/v2}TPX"
LXNS = "{http://www.garmin.com/xmlschemas/ActivityExtension/v2}LX"

class TCXPandas(object):
    """
    Class for Parsing .TCX files to Pandas DataFrames.

    Parameters
    ----------
    tcx_file : string, path object,
               the path to the tcx file

    """

    def __init__(self, tcx_file, **kwds):
        self.__filehandle__ = tcx_file
        self.tcx = None
        self.activity = None
        self.dataframe = None

        logging.basicConfig(filename="TCXconversion.log", level=logging.DEBUG)

    def parse(self):
        """
        Parse specified TCX file into a DataFrame
        Return a Dataframe and sets Dataframe and sets
        the self.dataframe object in the TCXParser.
        """

        self.tcx = objectify.parse(open(self.__filehandle__))
        self.activity = self.tcx.getroot().Activities.Activity
        self.dataframe = pd.DataFrame(self._traverse_laps_())
        return self.dataframe

    def get_activity_timestamp(self):
        """
        Returns the TCX file timestamp if parsed
        """
        if self.activity is None:
            return None
        else:
            return self.activity.Id

    def get_sport(self):
        """
        Returns the specified sport of the TCX file
        """
        if self.activity is None:
            return None
        else:
            return self.activity.attrib['Sport']

    def get_workout_startime(self):
        """
        Returns the starting timestamp of the specified TCX file
        """
        if self.activity is None:
            return None
        else:
            return self.activity.Lap.items()[0][1]

    def _traverse_laps_(self):

        # New iterator method to align with lxml standard
        return_array = []
        for laps in self.activity.Lap:
            for tracks in laps.Track:
                for trackingpoints in tracks.Trackpoint:
                    return_dict = {}
                    return_dict['time'] = dateutil.parser.parse(str(trackingpoints.Time))

                    try:
                        return_dict['latitude'] = \
                            np.float(trackingpoints.Position.LatitudeDegrees)
                    except AttributeError:
                        pass #TODO log this

                    try:
                        return_dict['longitude'] = \
                            np.float(trackingpoints.Position.LongitudeDegrees)
                    except AttributeError:
                        pass #TODO log this

                    try:
                        return_dict['altitude'] = np.float(trackingpoints.AltitudeMeters)
                    except AttributeError:
                        pass #TODO log this

                    try:
                        return_dict['distance'] = np.float(trackingpoints.DistanceMeters)
                    except AttributeError:
                        pass #TODO log this

                    try:
                        return_dict['hr'] = np.float(trackingpoints.HeartRateBpm.Value)
                    except AttributeError:
                        pass #TODO log this

                    try:
                        return_dict['speed'] = \
                            np.float(trackingpoints.Extensions[TPXNS].Speed)
                    except AttributeError:
                        pass #TODO log this

                    if self.get_sport == 'Running':
                        try:
                            return_dict['cadence'] = \
                                np.float(trackingpoints.Extensions[TPXNS].RunCadence)
                        except AttributeError:
                            pass #TODO log this
                    else: # self.activity.attrib['Sport'] == 'Biking':
                        try:
                            return_dict['cadence'] = np.float(trackingpoints.Cadence)
                        except AttributeError:
                            pass #TODO log this

                        try:
                            return_dict['power'] = \
                                np.float(trackingpoints.Extensions[TPXNS].Watts)
                        except AttributeError:
                            pass #TODO log this

                    return_array.append(return_dict)
                return return_array


#!/usr/bin/python

# import numpy as np
from matplotlib import pyplot as plt
from matplotlib.patches import Rectangle

class Dbg(object):
    def __init__(self):
        pass

    def return_bounding_box_2d(self, x, y, xsize, ysize):
        """Return the bounding box

        :param x: x center
        :param y: y center
        :param xsize: x size
        :param ysize: y size
        :return: list(x1, y1, x2, y2) where (x1, y1) and (x2, y2) are the coordinates of the diagonal points of the
                bounding box depending on your coordinates frame
        """

        if xsize <= 0 or ysize <= 0:
            print("ERROR: can't compute bounding box, xsize or height has no positive value")
            return []
        return [x-xsize/2, y-ysize/2, x+xsize/2, y+ysize/2]

    def compute_qsr(self, bb1, bb2):
        """Wrapper for __compute_qsr

        :param bb1: diagonal points coordinates of first bounding box (x1, y1, x2, y2)
        :param bb2: diagonal points coordinates of second bounding box (x1, y1, x2, y2)
        :return: an RCC depending on your implementation
        """
        return self.__compute_qsr(bb1, bb2)

    def __compute_qsr(self, bb1, bb2):
        """Replace with your own

        :param bb1: diagonal points coordinates of first bounding box (x1, y1, x2, y2)
        :param bb2: diagonal points coordinates of second bounding box (x1, y1, x2, y2)        :return: an RCC depending on your implementation
        :return: an RCC depending on your implementation
        """
        raise NotImplementedError("Replace with your code")

def plot_bbs(bb1, bb2):
    plt.figure()
    ax = plt.gca()
    # ax.invert_yaxis()
    ax.add_patch(Rectangle((bb1[0], bb1[1]), bb1[2]-bb1[0], bb1[3]-bb1[1], alpha=1, facecolor="blue"))
    ax.annotate("o1", (bb1[0], bb1[1]), color='black', weight='bold', fontsize=14)
    ax.add_patch(Rectangle((bb2[0], bb2[1]), bb2[2]-bb2[0], bb2[3]-bb2[1], alpha=1, facecolor="red"))
    ax.annotate("o2", (bb2[0], bb2[1]), color='black', weight='bold', fontsize=14)
    h = 6
    l = 0
    # ax.set_xlim(l, h)
    # ax.set_ylim(l, h)
    ax.set_xlim(l, h)
    ax.set_ylim(h, l)
    plt.show()

if __name__ == '__main__':
    dbg = Dbg()

    # Play with these to test (x_center, y_center, xsize(i.e. x-size), ysize(i.e. y-size))
    o1 = (2.0, 2.0, 2., 2.)
    o2 = (4.0, 3.0, 1., 1.)

    o1 = dbg.return_bounding_box_2d(o1[0], o1[1], o1[2], o1[3])
    o2 = dbg.return_bounding_box_2d(o2[0], o2[1], o2[2], o2[3])

    # Bounding boxes
    # print("o1:", o1)
    # print("o2:", o2)

    # Relations
    print("o1o2:", dbg.compute_qsr(o1, o2))
    print("o2o1:", dbg.compute_qsr(o2, o1))

    # Plot the boxes
    plot_bbs(o1, o2)


# Authors: Alexandre Gramfort <alexandre.gramfort@telecom-paristech.fr>
#          Denis Engemann <denis.engemann@gmail.com>
#          Martin Luessi <mluessi@nmr.mgh.harvard.edu>
#          Eric Larson <larson.eric.d@gmail.com>
#
# License: Simplified BSD

import os.path as op
import warnings
from collections import namedtuple

import numpy as np
from numpy.testing import assert_raises

from mne import io, read_events, Epochs
from mne import pick_channels_evoked
from mne.channels import read_layout
from mne.time_frequency.tfr import AverageTFR
from mne.utils import run_tests_if_main

from mne.viz import (plot_topo, plot_topo_image_epochs, _get_presser,
                     mne_analyze_colormap)
from mne.viz.topo import _plot_update_evoked_topo

# Set our plotters to test mode
import matplotlib
matplotlib.use('Agg')  # for testing don't use X server

warnings.simplefilter('always')  # enable b/c these tests throw warnings

base_dir = op.join(op.dirname(__file__), '..', '..', 'io', 'tests', 'data')
evoked_fname = op.join(base_dir, 'test-ave.fif')
raw_fname = op.join(base_dir, 'test_raw.fif')
event_name = op.join(base_dir, 'test-eve.fif')
event_id, tmin, tmax = 1, -0.2, 0.2
layout = read_layout('Vectorview-all')

def _get_raw():
    return io.Raw(raw_fname, preload=False)

def _get_events():
    return read_events(event_name)

def _get_picks(raw):
    return [0, 1, 2, 6, 7, 8, 340, 341, 342]  # take a only few channels

def _get_epochs():
    raw = _get_raw()
    events = _get_events()
    picks = _get_picks(raw)
    epochs = Epochs(raw, events[:10], event_id, tmin, tmax, picks=picks,
                    baseline=(None, 0))
    return epochs

def _get_epochs_delayed_ssp():
    raw = _get_raw()
    events = _get_events()
    picks = _get_picks(raw)
    reject = dict(mag=4e-12)
    epochs_delayed_ssp = Epochs(raw, events[:10], event_id, tmin, tmax,
                                picks=picks, baseline=(None, 0),
                                proj='delayed', reject=reject)
    return epochs_delayed_ssp

def test_plot_topo():
    """Test plotting of ERP topography
    """
    import matplotlib.pyplot as plt
    # Show topography
    evoked = _get_epochs().average()
    plot_topo(evoked)  # should auto-find layout
    warnings.simplefilter('always', UserWarning)
    picked_evoked = evoked.pick_channels(evoked.ch_names[:3], copy=True)
    picked_evoked_eeg = evoked.pick_types(meg=False, eeg=True, copy=True)
    picked_evoked_eeg.pick_channels(picked_evoked_eeg.ch_names[:3])

    # test scaling
    with warnings.catch_warnings(record=True):
        for ylim in [dict(mag=[-600, 600]), None]:
            plot_topo([picked_evoked] * 2, layout, ylim=ylim)

        for evo in [evoked, [evoked, picked_evoked]]:
            assert_raises(ValueError, plot_topo, evo, layout, color=['y', 'b'])

        evoked_delayed_ssp = _get_epochs_delayed_ssp().average()
        ch_names = evoked_delayed_ssp.ch_names[:3]  # make it faster
        picked_evoked_delayed_ssp = pick_channels_evoked(evoked_delayed_ssp,
                                                         ch_names)
        fig = plot_topo(picked_evoked_delayed_ssp, layout, proj='interactive')
        func = _get_presser(fig)
        event = namedtuple('Event', 'inaxes')
        func(event(inaxes=fig.axes[0]))
        params = dict(evokeds=[picked_evoked_delayed_ssp],
                      times=picked_evoked_delayed_ssp.times,
                      fig=fig, projs=picked_evoked_delayed_ssp.info['projs'])
        bools = [True] * len(params['projs'])
        _plot_update_evoked_topo(params, bools)
    # should auto-generate layout
    plot_topo(picked_evoked_eeg.copy(),
              fig_background=np.zeros((4, 3, 3)), proj=True)
    plt.close('all')

def test_plot_topo_image_epochs():
    """Test plotting of epochs image topography
    """
    import matplotlib.pyplot as plt
    title = 'ERF images - MNE sample data'
    epochs = _get_epochs()
    cmap = mne_analyze_colormap(format='matplotlib')
    plot_topo_image_epochs(epochs, sigma=0.5, vmin=-200, vmax=200,
                           colorbar=True, title=title, cmap=cmap)
    plt.close('all')

def test_plot_tfr_topo():
    """Test plotting of TFR data
    """
    epochs = _get_epochs()
    n_freqs = 3
    nave = 1
    data = np.random.RandomState(0).randn(len(epochs.ch_names),
                                          n_freqs, len(epochs.times))
    tfr = AverageTFR(epochs.info, data, epochs.times, np.arange(n_freqs), nave)
    tfr.plot_topo(baseline=(None, 0), mode='ratio', title='Average power',
                  vmin=0., vmax=14., show=False)
    tfr.plot([4], baseline=(None, 0), mode='ratio', show=False, title='foo')

run_tests_if_main()


#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""A Julia set computing workflow: https://en.wikipedia.org/wiki/Julia_set.

We use the quadratic polinomial f(z) = z*z + c, with c = -.62772 +.42193i
"""

from __future__ import absolute_import

import argparse

import apache_beam as beam
from apache_beam.io import WriteToText

def from_pixel(x, y, n):
  """Converts a NxN pixel position to a (-1..1, -1..1) complex number."""
  return complex(2.0 * x / n - 1.0, 2.0 * y / n - 1.0)

def get_julia_set_point_color(element, c, n, max_iterations):
  """Given an pixel, convert it into a point in our julia set."""
  x, y = element
  z = from_pixel(x, y, n)
  for i in xrange(max_iterations):
    if z.real * z.real + z.imag * z.imag > 2.0:
      break
    z = z * z + c
  return x, y, i  # pylint: disable=undefined-loop-variable

def generate_julia_set_colors(pipeline, c, n, max_iterations):
  """Compute julia set coordinates for each point in our set."""
  def point_set(n):
    for x in range(n):
      for y in range(n):
        yield (x, y)

  julia_set_colors = (pipeline
                      | 'add points' >> beam.Create(point_set(n))
                      | beam.Map(
                          get_julia_set_point_color, c, n, max_iterations))

  return julia_set_colors

def generate_julia_set_visualization(data, n, max_iterations):
  """Generate the pixel matrix for rendering the julia set as an image."""
  import numpy as np  # pylint: disable=wrong-import-order, wrong-import-position
  colors = []
  for r in range(0, 256, 16):
    for g in range(0, 256, 16):
      for b in range(0, 256, 16):
        colors.append((r, g, b))

  xy = np.zeros((n, n, 3), dtype=np.uint8)
  for x, y, iteration in data:
    xy[x, y] = colors[iteration * len(colors) / max_iterations]

  return xy

def save_julia_set_visualization(out_file, image_array):
  """Save the fractal image of our julia set as a png."""
  from matplotlib import pyplot as plt  # pylint: disable=wrong-import-order, wrong-import-position
  plt.imsave(out_file, image_array, format='png')

def run(argv=None):  # pylint: disable=missing-docstring

  parser = argparse.ArgumentParser()
  parser.add_argument('--grid_size',
                      dest='grid_size',
                      default=1000,
                      help='Size of the NxN matrix')
  parser.add_argument(
      '--coordinate_output',
      dest='coordinate_output',
      required=True,
      help='Output file to write the color coordinates of the image to.')
  parser.add_argument('--image_output',
                      dest='image_output',
                      default=None,
                      help='Output file to write the resulting image to.')
  known_args, pipeline_args = parser.parse_known_args(argv)

  p = beam.Pipeline(argv=pipeline_args)
  n = int(known_args.grid_size)

  coordinates = generate_julia_set_colors(p, complex(-.62772, .42193), n, 100)

  # Group each coordinate triplet by its x value, then write the coordinates to
  # the output file with an x-coordinate grouping per line.
  # pylint: disable=expression-not-assigned
  (coordinates
   | 'x coord key' >> beam.Map(lambda (x, y, i): (x, (x, y, i)))
   | 'x coord' >> beam.GroupByKey()
   | 'format' >> beam.Map(
       lambda (k, coords): ' '.join('(%s, %s, %s)' % coord for coord in coords))
   | WriteToText(known_args.coordinate_output))
  # pylint: enable=expression-not-assigned
  return p.run().wait_until_finish()

  # Optionally render the image and save it to a file.
  # TODO(silviuc): Add this functionality.
  # if p.options.image_output is not None:
  #  julia_set_image = generate_julia_set_visualization(
  #      file_with_coordinates, n, 100)
  #  save_julia_set_visualization(p.options.image_output, julia_set_image)


"""
Testing for the base module (sklearn.ensemble.base).
"""

# Authors: Gilles Louppe
# License: BSD 3 clause

from numpy.testing import assert_equal
from nose.tools import assert_true

from sklearn.utils.testing import assert_raise_message
from sklearn.datasets import load_iris
from sklearn.ensemble import BaggingClassifier
from sklearn.linear_model import Perceptron

def test_base():
    # Check BaseEnsemble methods.
    ensemble = BaggingClassifier(base_estimator=Perceptron(), n_estimators=3)

    iris = load_iris()
    ensemble.fit(iris.data, iris.target)
    ensemble.estimators_ = []  # empty the list and create estimators manually

    ensemble._make_estimator()
    ensemble._make_estimator()
    ensemble._make_estimator()
    ensemble._make_estimator(append=False)

    assert_equal(3, len(ensemble))
    assert_equal(3, len(ensemble.estimators_))

    assert_true(isinstance(ensemble[0], Perceptron))

def test_base_zero_n_estimators():
    # Check that instantiating a BaseEnsemble with n_estimators<=0 raises
    # a ValueError.
    ensemble = BaggingClassifier(base_estimator=Perceptron(), n_estimators=0)
    iris = load_iris()
    assert_raise_message(ValueError,
                         "n_estimators must be greater than zero, got 0.",
                         ensemble.fit, iris.data, iris.target)


from dummy.viewer.formatting import ResultFormatter, Formatter
import logging

logger = logging.getLogger( __name__ )

try:
	import pylab
	import numpy

	@Formatter.register( 'plot' )
	class PlotFormatter( ResultFormatter ):

		def __init__( self, *args, **kwargs ):
			super( PlotFormatter, self ).__init__( self, *args, **kwargs )

			# create the figure
			self.figure = pylab.figure( facecolor='white' )

		def format_results( self, results, *metrics ):
			self.setup( results )

			try:
				self.plot( results, metrics )
			except ( ValueError, TypeError ) as e:
				raise Exception(
					"Non numeric metrics cannot be plotted"
				)

		def setup( self, results ):
			# get the xlabels
			x = range( 1, len( results ) + 1 )
			xlabels = [ r.test.name for r in results ]

			pylab.title( 'Metric values per test (commit: %s)' % results[0].commit, fontsize=22 )
			pylab.xticks( rotation=90 )
			pylab.grid( True, markevery='integer' )
			pylab.xlabel( 'tests', fontsize=16 )
			pylab.margins( 0.05 )
			pylab.xticks( x, xlabels )

		def plot( self, results, metrics, **opts ):
			# create the plots
			plots = []
			for metric in metrics:
				plots.append( self.plot_metric( results, metric , **opts ))

			# legendary
			pylab.legend([ p[0] for p in plots], metrics )

			# and show it
			pylab.show()

		def plot_metric( self, results, metric, **opts ):
			x = range( 1, len( results ) + 1 )
			y = [ t.get_metric( metric ) for t in results ]

			try:
				plot = pylab.plot( x, y, **opts )
				pylab.setp( plot,
					label=metric,
					linestyle='dashed',
					linewidth=1.0,
					marker=".",
					markersize=12.0,
					aa=True
				)

				return plot
			except ( ValueError, TypeError ) as e:
				raise Exception(
					"The metric `%s` is not numeric and can thus not be plotted." % metric
				)

	@Formatter.register( 'plot.bar' )
	class BarPlotFormatter( PlotFormatter ):

		def plot( self, results, metrics, **opts ):
			# create the plots
			plots = []
			x = numpy.arange( len( results ))
			margin = 0.2 / len( metrics )
			width = 0.8 / len( metrics )
			colors = [
				( i/( 2 * len( metrics )), i/len(metrics), 0.8 )
				for i in range( 1, len( metrics ) + 1)
			]

			for i, metric in enumerate( metrics ):
				# compute the bar heights
				y = [ t.get_metric( metric ) or 0 for t in results ]

				plot = self.bar(
					x + 0.5 + i*width + ( i ) * margin,
					y,
					width=width,
					color=colors[i],
				)
				plots.append( plot )

				pylab.setp( plot,
					label=metric,
					aa=True
				)

			# legendary
			pylab.legend([ p[0] for p in plots], metrics )

			# and show it
			pylab.show()

		def bar( self, *args, **kwargs ):
			return pylab.bar( *args, **kwargs )

except ImportError:
	logger.debug( "matplotlib is not installed, PlotFormatter not available." )