docs: def get_sampler(
      data_table=None,
      p0=None,
      model=None,
      prior=None,
      nwalkers=500,
      nburn=100,
      guess=True,
      interactive=False,
      prefit=False,
      labels=None,
      threads=4,
      data_sed=None,
  ):
      """Generate a new MCMC sampler.
  
      Parameters
      ----------
      data_table : `~astropy.table.Table` or list of `~astropy.table.Table`
          Table containing the observed spectrum. If multiple tables are passed
          as a string, they will be concatenated in the order given. Each table
          needs at least these columns, with the appropriate associated units
          (with the physical type indicated in brackets below) as either a
          `~astropy.units.Unit` instance or parseable string:
  
          - ``energy``: Observed photon energy [``energy``]
          - ``flux``: Observed fluxes [``flux`` or ``differential flux``]
          - ``flux_error``: 68% CL gaussian uncertainty of the flux [``flux`` or
            ``differential flux``]. It can also be provided as ``flux_error_lo``
            and ``flux_error_hi`` (see below).
  
          Optional columns:
  
          - ``energy_width``: Width of the energy bin [``energy``], or
          - ``energy_error``: Half-width of the energy bin [``energy``], or
          - ``energy_error_lo`` and ``energy_error_hi``: Distance from bin center
            to lower and upper bin edges [``energy``], or
          - ``energy_lo`` and ``energy_hi``: Energy edges of the corresponding
            energy bin [``energy``]
          - ``flux_error_lo`` and ``flux_error_hi``: 68% CL gaussian lower and
            upper uncertainties of the flux.
          - ``ul``: Flag to indicate that a flux measurement is an upper limit.
          - ``flux_ul``: Upper limit to the flux. If not present, the ``flux``
            column will be taken as an upper limit for those measurements with
            the ``ul`` flag set to True or 1.
  
          The ``keywords`` metadata field of the table can be used to provide the
          confidence level of the upper limits with the keyword ``cl``, which
          defaults to 90%. The `astropy.io.ascii` reader can recover all
          the needed information from ASCII tables in the
          :class:`~astropy.io.ascii.Ipac` and :class:`~astropy.io.ascii.Daophot`
          formats, and everything except the ``cl`` keyword from tables in the
          :class:`~astropy.io.ascii.Sextractor`.  For the latter format, the cl
          keyword can be added after reading the table with::
  
              data.meta['keywords']['cl']=0.99
  
      p0 : array
          Initial position vector. The distribution for the ``nwalkers`` walkers
          will be computed as a multidimensional gaussian of width 5% around the
          initial position vector ``p0``.
      model : function
          A function that takes a vector in the parameter space and the data
          dictionary, and returns the expected fluxes at the energies in the
          spectrum. Additional return objects will be saved as blobs in the
          sampler chain, see `the emcee documentation for the
          format
          <http://dan.iel.fm/emcee/current/user/advanced/#arbitrary-metadata-blobs>`_.
      prior : function, optional
          A function that takes a vector in the parameter space and returns the
          log-likelihood of the Bayesian prior. Parameter limits can be specified
          through a uniform prior, returning 0. if the vector is within the
          parameter bounds and ``-np.inf`` otherwise.
      nwalkers : int, optional
          The number of Goodman & Weare “walkers”. Default is 500.
      nburn : int, optional
          Number of burn-in steps. After ``nburn`` steps, the sampler is reset
          and chain history discarded. It is necessary to settle the sampler into
          the maximum of the parameter space density. Default is 100.
      labels : iterable of strings, optional
          Labels for the parameters included in the position vector ``p0``. If
          not provided ``['par1','par2', ... ,'parN']`` will be used.
      threads : int, optional
          Number of threads to use for sampling. Default is 4.
      guess : bool, optional
          Whether to attempt to guess the normalization (first) parameter of the
          model. Default is True.
      interactive : bool, optional
          Whether to launch the interactive fitting window to set the initial
          values for the prefitting or the MCMC run. Requires matplotlib. Default
          is False.
      prefit : bool, optional
          Whether to attempt to find the maximum likelihood parameters with a
          Nelder-Mead algorithm and use them as starting point of the MCMC run.
          The parameter values in `p0` will be used as starting points for the
          minimization. Note that the initial optimization is done without taking
          the prior function into account to avoid the possibility of infinite
          values in the objective function. If the best-fit parameter vector
          without prior is forbidden by the prior given, it will be discarded.
      data_sed : bool, optional
          When providing more than one data table, whether to convert them to SED
          format. If unset or None, all tables will be converted to the format of
          the first table.
  
      Returns
      -------
      sampler : :class:`~emcee.EnsembleSampler` instance
          Ensemble sampler with walker positions after ``nburn`` burn-in steps.
      pos : :class:`~numpy.ndarray`
          Final position vector array.
  
      See also
      --------
      emcee.EnsembleSampler
      """
      import emcee
  
      if data_table is None:
          raise TypeError("Data table is missing!")
      else:
          data = validate_data_table(data_table, sed=data_sed)
  
      if model is None:
          raise TypeError("Model function is missing!")
  
      # Add parameter labels if not provided or too short
      if labels is None:
          # First is normalization
          labels = ["norm"] + ["par{0}".format(i) for i in range(1, len(p0))]
      elif len(labels) < len(p0):
          labels += ["par{0}".format(i) for i in range(len(labels), len(p0))]
  
      # Check that the model returns fluxes in same physical type as data
      modelout = model(p0, data)
      if (type(modelout) == tuple or type(modelout) == list) and (
          type(modelout) != np.ndarray
      ):
          spec = modelout[0]
      else:
          spec = modelout
  
      # check whether both can be converted to same physical type through
      # sed_conversion
      try:
          # If both can be converted to differential flux, they can be compared
          # Otherwise, sed_conversion will raise a u.UnitsError
          sed_conversion(data["energy"], spec.unit, False)
          sed_conversion(data["energy"], data["flux"].unit, False)
      except u.UnitsError:
          raise u.UnitsError(
              "The physical type of the model and data units are not compatible,"
              " please modify your model or data so they match:\n"
              " Model units: {0} [{1}]\n Data units: {2} [{3}]\n".format(
                  spec.unit,
                  spec.unit.physical_type,
                  data["flux"].unit,
                  data["flux"].unit.physical_type,
              )
          )
  
      if guess:
          normNames = ["norm", "Norm", "ampl", "Ampl", "We", "Wp"]
          normNameslog = ["log({0}".format(name) for name in normNames]
          normNameslog10 = ["log10({0}".format(name) for name in normNames]
          normNames += normNameslog + normNameslog10
          idxs = []
          for l in normNames:
              for l2 in labels:
                  if l2.startswith(l):
                      # check with startswith to include normalization,
                      # amplitude, etc.
                      idxs.append(labels.index(l2))
  
          if len(idxs) == 1:
  
              nunit, sedf = sed_conversion(data["energy"], spec.unit, False)
              currFlux = np.trapz(
                  data["energy"] * (spec * sedf).to(nunit), data["energy"]
              )
              nunit, sedf = sed_conversion(
                  data["energy"], data["flux"].unit, False
              )
              dataFlux = np.trapz(
                  data["energy"] * (data["flux"] * sedf).to(nunit),
                  data["energy"],
              )
              ratio = dataFlux / currFlux
              if labels[idxs[0]].startswith("log("):
                  p0[idxs[0]] += np.log(ratio)
              elif labels[idxs[0]].startswith("log10("):
                  p0[idxs[0]] += np.log10(ratio)
              else:
                  p0[idxs[0]] *= ratio
  
          elif len(idxs) == 0:
              log.warning(
                  "No label starting with [{0}] found: not applying"
                  " normalization guess.".format(",".join(normNames))
              )
          elif len(idxs) > 1:
              log.warning(
                  "More than one label starting with [{0}] found:"
                  " not applying normalization guess.".format(
                      ",".join(normNames)
                  )
              )
  
      P0_IS_ML = False
      if interactive:
          try:
              log.info("Launching interactive model fitter, close when finished")
              from .model_fitter import InteractiveModelFitter
              import matplotlib.pyplot as plt
  
              iprev = plt.rcParams["interactive"]
              plt.rcParams["interactive"] = False
              imf = InteractiveModelFitter(
                  model, p0, data, labels=labels, sed=True
              )
              p0 = imf.pars
              P0_IS_ML = imf.P0_IS_ML
              plt.rcParams["interactive"] = iprev
          except ImportError as e:
              log.warning(
                  "Interactive fitting is not available because"
                  " matplotlib is not installed: {0}".format(e)
              )
  
      # If we already did the prefit call in ModelWidget (and didn't modify the
      # parameters afterwards), avoid doing it here
      if prefit and not P0_IS_ML:
          p0, P0_IS_ML = _prefit(p0, data, model, prior)
  
      sampler = emcee.EnsembleSampler(
          nwalkers, len(p0), lnprob, args=[data, model, prior], threads=threads
      )
  
      # Add data and parameters properties to sampler
      sampler.data_table = data_table
      sampler.data = data
      sampler.labels = labels
      # Add model function to sampler
      sampler.modelfn = model
      # Add run_info dict
      sampler.run_info = {
          "n_walkers": nwalkers,
          "n_burn": nburn,
          # convert from np.float to regular float
          "p0": [float(p) for p in p0],
          "guess": guess,
      }
  
      # Initialize walkers in a ball of relative size 0.5% in all dimensions if
      # the parameters have been fit to their ML values, or to 10% otherwise
      spread = 0.005 if P0_IS_ML else 0.1
      p0var = np.array([spread * pp for pp in p0])
      p0 = emcee.utils.sample_ball(p0, p0var, nwalkers)
  
      if nburn > 0:
          print(
              "Burning in the {0} walkers with {1} steps...".format(
                  nwalkers, nburn
              )
          )
          sampler, pos = _run_mcmc(sampler, p0, nburn)
      else:
          pos = p0
  
      sampler.run_info["p0_burn_median"] = [
          float(p) for p in np.median(pos, axis=0)
      ]
  
      return sampler, pos