<commit_before>from __future__ import division
from warnings import warn
import numpy as np
import scipy.optimize as opt
from .recspeed import local_maxima, remove_similar_vertices, search_descending
from ..core.onetime import auto_attr
from dipy.core.sphere import HemiSphere, Sphere, unique_edges, unit_icosahedron
#Classes OdfModel and OdfFit are using API ReconstModel and ReconstFit from .base 

default_sphere = HemiSphere.from_sphere(unit_icosahedron.subdivide(3))

class DirectionFinder(object):
    """Abstract class for direction finding"""

    def __init__(self):
        self._config = {}

    def __call__(self, sphere_eval):
        """To be impemented by subclasses"""
        raise NotImplementedError()

    def config(self, **kwargs):
        """Update direction finding parameters"""
        for i in kwargs:
            if i not in self._config:
                warn("{} is not a known parameter".format(i))
        self._config.update(kwargs)


class DiscreteDirectionFinder(DirectionFinder):
    """Discrete Direction Finder

    Parameters
    ----------
    sphere : Sphere
        The Sphere providing discrete directions for evaluation.
    relative_peak_threshold : float
        Only return peaks greater than ``relative_peak_threshold * m`` where m
        is the largest peak.
    min_separation_angle : float in [0, 90]
        The minimum distance between directions. If two peaks are too close only
        the larger of the two is returned.

    """
    def __init__(self, sphere=default_sphere, relative_peak_threshold=.25,
                 min_separation_angle=45):
        self._config = {"sphere": sphere,
                        "relative_peak_threshold": relative_peak_threshold,
                        "min_separation_angle": min_separation_angle}

    def __call__(self, sphere_eval):
        """Find directions of a function evaluated on a discrete sphere

        Parameters
        ----------
        sphere_eval : callable
            The function to maximize, should evaluate over a sphere.

        Returns
        -------
        directions : ndarray (N, 3)
            The directions of the N peaks.

        """
        sphere = self._config["sphere"]
        relative_peak_threshold = self._config["relative_peak_threshold"]
        min_separation_angle = self._config["min_separation_angle"]
        discrete_values = sphere_eval(sphere)
        return peak_directions(discrete_values, sphere,
                               relative_peak_threshold, min_separation_angle)


def _nl_peak_finder(sphere_eval, seeds, fmin, gtol):
    """Non-linear search for peaks from each seed
    """
    # Helper function
    def _helper(x):
        sphere = Sphere(theta=x[0], phi=x[1])
        return -sphere_eval(sphere)

    # Non-linear search
    theta = np.zeros(len(seeds))
    phi = np.zeros(len(seeds))
    for i in xrange(len(seeds)):
        peak = fmin(_helper, seeds[i], gtol=gtol, disp=False)
        theta[i], phi[i] = peak
    # Return one peak for each seed
    return theta, phi


class NonLinearDirectionFinder(DirectionFinder):
    """Non Linear Direction Finder

    Parameters
    ----------
    sphere : Sphere
        The Sphere providing discrete directions for evaluation.
    relative_peak_threshold : float
        Only return peaks greater than ``relative_peak_threshold * m`` where m
        is the largest peak.
    min_separation_angle : float in [0, 90]
        The minimum distance between directions. If two peaks are too close only
        the larger of the two is returned.
    fmin : optimization function, optional
        By default use scipy.optimize.fmin_bfgs.
    gtol : float
        Optimize up to gradient tolerance.

    """
    def __init__(self, sphere=default_sphere, relative_peak_threshold=.25,
                 min_separation_angle=45, fmin=opt.fmin_bfgs, gtol=1e-6):
        self._config = {"sphere": sphere,
                        "relative_peak_threshold": relative_peak_threshold,
                        "min_separation_angle": min_separation_angle,
                        "fmin":fmin, "gtol":gtol}

    def __call__(self, sphere_eval):
        """Find directions of a function evaluated on a discrete sphere

        Parameters
        ----------
        sphere_eval : callable
            The function to maximize, should evaluate over a sphere.

        Returns
        -------
        directions : ndarray (N, 3)
            The directions of the N peaks.

        """
        # Get parameters from _config
        sphere = self._config["sphere"]
        relative_peak_threshold = self._config["relative_peak_threshold"]
        min_separation_angle = self._config["min_separation_angle"]
        fmin = self._config["fmin"]
        gtol = self._config["gtol"]

        # Find discrete peaks for use as seeds in lon-linear search
        discrete_values = sphere_eval(sphere)
        values, indices = local_maxima(discrete_values, sphere.edges)
        seeds = np.column_stack([sphere.theta[indices], sphere.phi[indices]])

        # Non-linear search
        peak_theta, peak_phi = _nl_peak_finder(sphere_eval, seeds, fmin, gtol)

        # Evaluate on new-found peaks
        small_sphere = Sphere(theta=peak_theta, phi=peak_phi)
        values = sphere_eval(small_sphere)
        # Sort in descending order
        order = values.argsort()[::-1]
        values = values[order]
        vertices = small_sphere.vertices[order]
        # Remove small peaks
        n = search_descending(values, relative_peak_threshold)
        directions = vertices[:n]
        # Remove peaks too close to each-other
        directions = remove_similar_vertices(directions, min_separation_angle,)
        return directions

class OdfModel(object):
    """An abstract class to be sub-classed by specific odf models

    All odf models should provide a fit method which may take data as it's
    first and only argument.
    """
    direction_finder = DiscreteDirectionFinder()
    def fit(self, data):
        """To be implemented by specific odf models"""
        raise NotImplementedError("To be implemented in sub classes")

class OdfFit(object):
    def odf(self, sphere):
        """To be implemented but specific odf models"""
        raise NotImplementedError("To be implemented in sub classes")

    @auto_attr
    def directions(self):
        return self.model.direction_finder(self.odf)


def peak_directions(odf, sphere, relative_peak_threshold,
                    min_separation_angle):
    """Get the directions of odf peaks<commit_msg>BF - remove small peaks sooner to avoid over seeding non-linear search.<commit_after>from __future__ import division
from warnings import warn
import numpy as np
import scipy.optimize as opt
from .recspeed import local_maxima, remove_similar_vertices, search_descending
from ..core.onetime import auto_attr
from dipy.core.sphere import HemiSphere, Sphere, unique_edges, unit_icosahedron
#Classes OdfModel and OdfFit are using API ReconstModel and ReconstFit from .base 

default_sphere = HemiSphere.from_sphere(unit_icosahedron.subdivide(3))

class DirectionFinder(object):
    """Abstract class for direction finding"""

    def __init__(self):
        self._config = {}

    def __call__(self, sphere_eval):
        """To be impemented by subclasses"""
        raise NotImplementedError()

    def config(self, **kwargs):
        """Update direction finding parameters"""
        for i in kwargs:
            if i not in self._config:
                warn("{} is not a known parameter".format(i))
        self._config.update(kwargs)


class DiscreteDirectionFinder(DirectionFinder):
    """Discrete Direction Finder

    Parameters
    ----------
    sphere : Sphere
        The Sphere providing discrete directions for evaluation.
    relative_peak_threshold : float
        Only return peaks greater than ``relative_peak_threshold * m`` where m
        is the largest peak.
    min_separation_angle : float in [0, 90]
        The minimum distance between directions. If two peaks are too close only
        the larger of the two is returned.

    """
    def __init__(self, sphere=default_sphere, relative_peak_threshold=.25,
                 min_separation_angle=45):
        self._config = {"sphere": sphere,
                        "relative_peak_threshold": relative_peak_threshold,
                        "min_separation_angle": min_separation_angle}

    def __call__(self, sphere_eval):
        """Find directions of a function evaluated on a discrete sphere

        Parameters
        ----------
        sphere_eval : callable
            The function to maximize, should evaluate over a sphere.

        Returns
        -------
        directions : ndarray (N, 3)
            The directions of the N peaks.

        """
        sphere = self._config["sphere"]
        relative_peak_threshold = self._config["relative_peak_threshold"]
        min_separation_angle = self._config["min_separation_angle"]
        discrete_values = sphere_eval(sphere)
        return peak_directions(discrete_values, sphere,
                               relative_peak_threshold, min_separation_angle)


def _nl_peak_finder(sphere_eval, seeds, fmin, gtol):
    """Non-linear search for peaks from each seed
    """
    # Helper function
    def _helper(x):
        sphere = Sphere(theta=x[0], phi=x[1])
        return -sphere_eval(sphere)

    # Non-linear search
    theta = np.zeros(len(seeds))
    phi = np.zeros(len(seeds))
    for i in xrange(len(seeds)):
        peak = fmin(_helper, seeds[i], gtol=gtol, disp=False)
        theta[i], phi[i] = peak
    # Return one peak for each seed
    return theta, phi


class NonLinearDirectionFinder(DirectionFinder):
    """Non Linear Direction Finder

    Parameters
    ----------
    sphere : Sphere
        The Sphere providing discrete directions for evaluation.
    relative_peak_threshold : float
        Only return peaks greater than ``relative_peak_threshold * m`` where m
        is the largest peak.
    min_separation_angle : float in [0, 90]
        The minimum distance between directions. If two peaks are too close only
        the larger of the two is returned.
    fmin : optimization function, optional
        By default use scipy.optimize.fmin_bfgs.
    gtol : float
        Optimize up to gradient tolerance.

    """
    def __init__(self, sphere=default_sphere, relative_peak_threshold=.25,
                 min_separation_angle=45, fmin=opt.fmin_bfgs, gtol=1e-6):
        self._config = {"sphere": sphere,
                        "relative_peak_threshold": relative_peak_threshold,
                        "min_separation_angle": min_separation_angle,
                        "fmin":fmin, "gtol":gtol}

    def __call__(self, sphere_eval):
        """Find directions of a function evaluated on a discrete sphere

        Parameters
        ----------
        sphere_eval : callable
            The function to maximize, should evaluate over a sphere.

        Returns
        -------
        directions : ndarray (N, 3)
            The directions of the N peaks.

        """
        # Get parameters from _config
        sphere = self._config["sphere"]
        relative_peak_threshold = self._config["relative_peak_threshold"]
        min_separation_angle = self._config["min_separation_angle"]
        fmin = self._config["fmin"]
        gtol = self._config["gtol"]

        # Find discrete peaks for use as seeds in lon-linear search
        discrete_values = sphere_eval(sphere)
        values, indices = local_maxima(discrete_values, sphere.edges)
        n = search_descending(values, relative_peak_threshold)
        indices = indices[:n]
        seeds = np.column_stack([sphere.theta[indices], sphere.phi[indices]])

        # Non-linear search
        peak_theta, peak_phi = _nl_peak_finder(sphere_eval, seeds, fmin, gtol)

        # Evaluate on new-found peaks
        small_sphere = Sphere(theta=peak_theta, phi=peak_phi)
        values = sphere_eval(small_sphere)
        # Sort in descending order
        order = values.argsort()[::-1]
        values = values[order]
        directions = small_sphere.vertices[order]
        # Remove peaks too close to each-other
        directions = remove_similar_vertices(directions, min_separation_angle,)
        return directions

class OdfModel(object):
    """An abstract class to be sub-classed by specific odf models

    All odf models should provide a fit method which may take data as it's
    first and only argument.
    """
    direction_finder = DiscreteDirectionFinder()
    def fit(self, data):
        """To be implemented by specific odf models"""
        raise NotImplementedError("To be implemented in sub classes")

class OdfFit(object):
    def odf(self, sphere):
        """To be implemented but specific odf models"""
        raise NotImplementedError("To be implemented in sub classes")

    @auto_attr
    def directions(self):
        return self.model.direction_finder(self.odf)


def peak_directions(odf, sphere, relative_peak_threshold,
                    min_separation_angle):
    """Get the directions of odf peaks
{'fraction_non_alphanumeric': 0.08468757152666515, 'fraction_numerical': 0.003356984817273213, 'mean_word_length': 2.6060522696011006, 'pattern_counts': {'":': 16, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '190183', 'n_tokens_mistral': 3663, 'n_tokens_neox': 3491, 'n_words': 1082}