def bin(x, bins, maxX=None, minX=None):
    '''
    bin signal x using 'binsN' bin. If minX, maxX are None, they default to the full
    range of the signal. If they are not None, everything above maxX gets assigned to
    binsN-1 and everything below minX gets assigned to 0, this is effectively the same
    as clipping x before passing it to 'bin'
    input:
    -----
        x:      signal to be binned, some sort of iterable
        bins:   int, number of bins
                iterable, bin edges
        maxX:   clips data above maxX
        minX:   clips data below maxX
    output:
    ------
        binnedX:    x after being binned
        bins:       bins used for binning.
                    if input 'bins' is already an iterable it just returns the
                    same iterable
    example:
        bin(x, 10)
        entropy)
        binsN = 10
        percentiles = list(np.arange(0, 100.1, 100/binsN))
        bins = np.percentile(x, percentiles)
        bin(x, bins)
    '''
    if maxX is None:
        maxX = x.max()
    if minX is None:
        minX = x.min()
    if not np.iterable(bins):
        bins = np.linspace(minX, maxX+1e-5, bins+1)
    return np.digitize(x.ravel(), bins).reshape(x.shape), bins