docs: def _bird_core(X, scales, n_runs, Lambda_W, max_iter=100,
                 stop_crit=np.mean,
                 selection_rule=np.sum,
                 n_jobs=1, indep=True,
                 random_state=None, memory=Memory(None), verbose=False):
      """Automatically detect when noise zone has been reached and stop
      MP at this point
  
      Parameters
      ----------
      X : array, shape (n_channels, n_times)
          The numpy n_channels-vy-N array to be denoised where n_channels is
          number of sensors and N the dimension
      scales : list
          The list of MDCT scales that will be used to built the
          dictionary Phi
      n_runs : int
          the number of runs (n_runs in the paper)
      Lambda_W : float
          bound for lambda under which a run will be stopped
      max_iter : int
          Maximum number of iterations (serves as alternate stopping criterion)
      stop_crit : function
          controls the calculation of Lambda
      selection_rule : callable
          controls the way multiple channel projections are combined for atom
          selection only used if indep=False
      n_jobs : int
          number of jobs to run in parallel
      indep : bool
          True for BIRD (independent processing of each channel,
          False for S-BIRD (structured sparsity seeked)
      random_state : None | int | np.random.RandomState
          To specify the random generator state (seed).
      memory : instance of Memory
          The object to use to cache some computations. If cachedir is None, no
          caching is performed.
      verbose : bool
          verbose mode
  
      Returns
      -------
      X_denoise : array, shape (n_channels, n_times)
          denoised array of same shape as X
      """
      Phi = MDCT(scales)
      pad = int(1.5 * max(scales))
      X_denoise = np.zeros_like(X)
      approx = []
      rng = check_random_state(random_state)
      seeds = rng.randint(4294967295, size=n_runs)  # < max seed value
  
      if n_jobs <= 0:
          n_cores = multiprocessing.cpu_count()
          n_jobs = min(n_cores + n_jobs + 1, n_cores)
  
      if indep:
          # Independent treat of each channel (plain BIRD)
          for r, x in zip(X_denoise, X):
              this_approx = Parallel(n_jobs=n_jobs)(
                          delayed(_denoise)(this_seeds, x, Phi, Lambda_W,
                                            max_iter, pad=pad, verbose=verbose,
                                            indep=True, memory=memory)
                                            for this_seeds in
                                            np.array_split(seeds, n_jobs))
              this_approx = sum(this_approx[1:], this_approx[0])
              r[:] = sum([a[pad:-pad] for a in this_approx])
              approx.append(this_approx)
      else:
          # data need to be processed jointly
          this_approx = Parallel(n_jobs=n_jobs)(
                          delayed(_denoise)(this_seeds, X, Phi, Lambda_W,
                                            max_iter, pad=pad, verbose=verbose,
                                            selection_rule=selection_rule,
                                            indep=False, memory=memory,
                                            stop_crit=stop_crit)
                                            for this_seeds in
                                            np.array_split(seeds, n_jobs))
  
          # reconstruction by averaging
          for jidx in range(len(this_approx)):
              for ridx in range(len(this_approx[jidx])):
                  X_denoise += this_approx[jidx][ridx]
  
      X_denoise /= float(n_runs)
      return X_denoise