code: 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