Repository: flarefly/flarefly
Pull request #118: Refactor PDF creation
Author: login: fchinu
Created: 2025-04-17T13:24:21Z | Merged: 2025-04-22T12:17:29Z

With this PR the code for the creation of the PDF has been refactored. This should simplify the support for additional PDFs, and also the implementation of the possibility to fit using ROOFit. Please let me know if this change can fit in the project.
When writing this code, I realised that the `mass` parameter of the `powlaw` background function is set to fixed by default, is this expected? If yes, I can make slight changes to the `pdf_config.py` file to make it the only parameter fixed by default.


Diff:
diff --git a/flarefly/fitter.py b/flarefly/fitter.py
index 4fe35b6..302ae47 100644
--- a/flarefly/fitter.py
+++ b/flarefly/fitter.py
@@ -15,6 +15,7 @@
 from hepstats.splot import compute_sweights
 import pdg
 from flarefly.utils import Logger
+from flarefly.pdf_builder import PDFBuilder
 import flarefly.custom_pdfs as cpdf
 
 
@@ -264,462 +265,31 @@ def __build_signal_pdfs(self, obs):
             if pdf_name == 'nosignal':
                 Logger('Performing fit with no signal pdf', 'WARNING')
                 break
-            if pdf_name == 'gaussian':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._signal_pdf_[ipdf] = zfit.pdf.Gauss(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigma=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}']
-                )
-            elif pdf_name == 'doublegaus':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigma1', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('sigma2', 0.100)
-                self._init_sgn_pars_[ipdf].setdefault('frac1', 0.9)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma1', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma2', False)
-                self._fix_sgn_pars_[ipdf].setdefault('frac1', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma1', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma2', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('frac1', [0., 1.])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma1_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma1_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma1'],
-                    self._limits_sgn_pars_[ipdf]['sigma1'][0], self._limits_sgn_pars_[ipdf]['sigma1'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma1'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma2_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma2_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma2'],
-                    self._limits_sgn_pars_[ipdf]['sigma2'][0], self._limits_sgn_pars_[ipdf]['sigma2'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma1'])
-                self._sgn_pars_[ipdf][f'{self._name_}_frac1_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_frac1_signal{ipdf}', self._init_sgn_pars_[ipdf]['frac1'],
-                    self._limits_sgn_pars_[ipdf]['frac1'][0], self._limits_sgn_pars_[ipdf]['frac1'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['frac1'])
-                self._signal_pdf_[ipdf] = cpdf.DoubleGauss(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigma1=self._sgn_pars_[ipdf][f'{self._name_}_sigma1_signal{ipdf}'],
-                    sigma2=self._sgn_pars_[ipdf][f'{self._name_}_sigma2_signal{ipdf}'],
-                    frac1=self._sgn_pars_[ipdf][f'{self._name_}_frac1_signal{ipdf}'],
-                )
-            elif pdf_name == 'crystalball':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('alpha', 0.5)
-                self._init_sgn_pars_[ipdf].setdefault('n', 1.)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alpha', False)
-                self._fix_sgn_pars_[ipdf].setdefault('n', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('alpha', [None, None])
-                self._limits_sgn_pars_[ipdf].setdefault('n', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alpha_signal{ipdf}', self._init_sgn_pars_[ipdf]['alpha'],
-                    self._limits_sgn_pars_[ipdf]['alpha'][0], self._limits_sgn_pars_[ipdf]['alpha'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alpha'])
-                self._sgn_pars_[ipdf][f'{self._name_}_n_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_n_signal{ipdf}', self._init_sgn_pars_[ipdf]['n'],
-                    self._limits_sgn_pars_[ipdf]['n'][0], self._limits_sgn_pars_[ipdf]['n'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['n'])
-                self._signal_pdf_[ipdf] = zfit.pdf.CrystalBall(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigma=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    alpha=self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'],
-                    n=self._sgn_pars_[ipdf][f'{self._name_}_n_signal{ipdf}']
-                )
-            elif pdf_name == 'doublecb':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('alphal', 0.5)
-                self._init_sgn_pars_[ipdf].setdefault('nl', 1.)
-                self._init_sgn_pars_[ipdf].setdefault('alphar', 0.5)
-                self._init_sgn_pars_[ipdf].setdefault('nr', 1.)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alphal', False)
-                self._fix_sgn_pars_[ipdf].setdefault('nl', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alphar', False)
-                self._fix_sgn_pars_[ipdf].setdefault('nr', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('alphal', [None, None])
-                self._limits_sgn_pars_[ipdf].setdefault('nl', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('alphar', [None, None])
-                self._limits_sgn_pars_[ipdf].setdefault('nr', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alphal_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alphal_signal{ipdf}', self._init_sgn_pars_[ipdf]['alphal'],
-                    self._limits_sgn_pars_[ipdf]['alphal'][0], self._limits_sgn_pars_[ipdf]['alphal'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alphal'])
-                self._sgn_pars_[ipdf][f'{self._name_}_nl_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_nl_signal{ipdf}', self._init_sgn_pars_[ipdf]['nl'],
-                    self._limits_sgn_pars_[ipdf]['nl'][0], self._limits_sgn_pars_[ipdf]['nl'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['nl'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alphar_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alphar_signal{ipdf}', self._init_sgn_pars_[ipdf]['alphar'],
-                    self._limits_sgn_pars_[ipdf]['alphar'][0], self._limits_sgn_pars_[ipdf]['alphar'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alphar'])
-                self._sgn_pars_[ipdf][f'{self._name_}_nr_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_nr_signal{ipdf}', self._init_sgn_pars_[ipdf]['nr'],
-                    self._limits_sgn_pars_[ipdf]['nr'][0], self._limits_sgn_pars_[ipdf]['nr'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['nr'])
-                self._signal_pdf_[ipdf] = zfit.pdf.DoubleCB(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigma=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    alphal=self._sgn_pars_[ipdf][f'{self._name_}_alphal_signal{ipdf}'],
-                    nl=self._sgn_pars_[ipdf][f'{self._name_}_nl_signal{ipdf}'],
-                    alphar=self._sgn_pars_[ipdf][f'{self._name_}_alphar_signal{ipdf}'],
-                    nr=self._sgn_pars_[ipdf][f'{self._name_}_nr_signal{ipdf}']
-                )
-            elif pdf_name == 'doublecbsymm':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('alpha', 0.5)
-                self._init_sgn_pars_[ipdf].setdefault('n', 1.)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alpha', False)
-                self._fix_sgn_pars_[ipdf].setdefault('n', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, 1.e6])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., 1.e6])
-                self._limits_sgn_pars_[ipdf].setdefault('alpha', [0, 1.e6])
-                self._limits_sgn_pars_[ipdf].setdefault('n', [0., 1.e6])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alpha_signal{ipdf}', self._init_sgn_pars_[ipdf]['alpha'],
-                    self._limits_sgn_pars_[ipdf]['alpha'][0], self._limits_sgn_pars_[ipdf]['alpha'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alpha'])
-                self._sgn_pars_[ipdf][f'{self._name_}_n_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_n_signal{ipdf}', self._init_sgn_pars_[ipdf]['n'],
-                    self._limits_sgn_pars_[ipdf]['n'][0], self._limits_sgn_pars_[ipdf]['n'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['n'])
-                self._signal_pdf_[ipdf] = zfit.pdf.DoubleCB(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigma=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    alphal=self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'],
-                    nl=self._sgn_pars_[ipdf][f'{self._name_}_n_signal{ipdf}'],
-                    alphar=self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'],
-                    nr=self._sgn_pars_[ipdf][f'{self._name_}_n_signal{ipdf}']
-                )
-            elif pdf_name == 'cauchy':
-                self._init_sgn_pars_[ipdf].setdefault('m', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('gamma', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('m', False)
-                self._fix_sgn_pars_[ipdf].setdefault('gamma', False)
-                self._limits_sgn_pars_[ipdf].setdefault('m', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('gamma', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_m_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_m_signal{ipdf}', self._init_sgn_pars_[ipdf]['m'],
-                    self._limits_sgn_pars_[ipdf]['m'][0], self._limits_sgn_pars_[ipdf]['m'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['m'])
-                self._sgn_pars_[ipdf][f'{self._name_}_gamma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_gamma_signal{ipdf}', self._init_sgn_pars_[ipdf]['gamma'],
-                    self._limits_sgn_pars_[ipdf]['gamma'][0], self._limits_sgn_pars_[ipdf]['gamma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['gamma'])
-                self._signal_pdf_[ipdf] = zfit.pdf.Cauchy(
-                    obs=obs,
-                    m=self._sgn_pars_[ipdf][f'{self._name_}_m_signal{ipdf}'],
-                    gamma=self._sgn_pars_[ipdf][f'{self._name_}_gamma_signal{ipdf}']
-                )
-            elif pdf_name == 'voigtian':
-                self._init_sgn_pars_[ipdf].setdefault('m', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('gamma', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('m', False)
-                self._fix_sgn_pars_[ipdf].setdefault('gamma', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._limits_sgn_pars_[ipdf].setdefault('m', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('gamma', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_m_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_m_signal{ipdf}', self._init_sgn_pars_[ipdf]['m'],
-                    self._limits_sgn_pars_[ipdf]['m'][0], self._limits_sgn_pars_[ipdf]['m'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['m'])
-                self._sgn_pars_[ipdf][f'{self._name_}_gamma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_gamma_signal{ipdf}', self._init_sgn_pars_[ipdf]['gamma'],
-                    self._limits_sgn_pars_[ipdf]['gamma'][0], self._limits_sgn_pars_[ipdf]['gamma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['gamma'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._signal_pdf_[ipdf] = zfit.pdf.Voigt(
-                    obs=obs,
-                    m=self._sgn_pars_[ipdf][f'{self._name_}_m_signal{ipdf}'],
-                    sigma=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    gamma=self._sgn_pars_[ipdf][f'{self._name_}_gamma_signal{ipdf}']
-                )
-            elif pdf_name == 'gausexptail':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('alpha', 1.e6)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alpha', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('alpha', [None, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alpha_signal{ipdf}', self._init_sgn_pars_[ipdf]['alpha'],
-                    self._limits_sgn_pars_[ipdf]['alpha'][0], self._limits_sgn_pars_[ipdf]['alpha'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alpha'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._signal_pdf_[ipdf] = zfit.pdf.GaussExpTail(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigma=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    alpha=self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}']
+            if 'kde' in pdf_name:
+                self._signal_pdf_[ipdf] = PDFBuilder.build_signal_kde(
+                    pdf_name,
+                    self._kde_signal_sample_[ipdf],
+                    self._name_,
+                    ipdf,
+                    self._kde_signal_option_[ipdf]
                 )
-            elif pdf_name == 'genergausexptail':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('alphar', 1.e6)
-                self._init_sgn_pars_[ipdf].setdefault('alphal', 1.e6)
-                self._init_sgn_pars_[ipdf].setdefault('sigmal', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('sigmar', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alphar', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alphal', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigmar', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigmal', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('alphar', [None, None])
-                self._limits_sgn_pars_[ipdf].setdefault('alphal', [None, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigmar', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigmal', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alphar_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alphar_signal{ipdf}', self._init_sgn_pars_[ipdf]['alphar'],
-                    self._limits_sgn_pars_[ipdf]['alphar'][0], self._limits_sgn_pars_[ipdf]['alphar'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alphar'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alphal_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alphal_signal{ipdf}', self._init_sgn_pars_[ipdf]['alphal'],
-                    self._limits_sgn_pars_[ipdf]['alphal'][0], self._limits_sgn_pars_[ipdf]['alphal'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alphal'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigmar_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigmar_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigmar'],
-                    self._limits_sgn_pars_[ipdf]['sigmar'][0], self._limits_sgn_pars_[ipdf]['sigmar'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigmar'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigmal_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigmal_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigmal'],
-                    self._limits_sgn_pars_[ipdf]['sigmal'][0], self._limits_sgn_pars_[ipdf]['sigmal'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigmal'])
-                self._signal_pdf_[ipdf] = zfit.pdf.GeneralizedGaussExpTail(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigmar=self._sgn_pars_[ipdf][f'{self._name_}_sigmar_signal{ipdf}'],
-                    sigmal=self._sgn_pars_[ipdf][f'{self._name_}_sigmal_signal{ipdf}'],
-                    alphar=self._sgn_pars_[ipdf][f'{self._name_}_alphar_signal{ipdf}'],
-                    alphal=self._sgn_pars_[ipdf][f'{self._name_}_alphal_signal{ipdf}']
-                )
-            elif pdf_name == 'genergausexptailsymm':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('alpha', 1.e6)
-                self._init_sgn_pars_[ipdf].setdefault('sigma', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alpha', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigma', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, 1.e6])
-                self._limits_sgn_pars_[ipdf].setdefault('alpha', [-1.e10, 1.e10])
-                self._limits_sgn_pars_[ipdf].setdefault('sigma', [0., 1.e6])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alpha_signal{ipdf}', self._init_sgn_pars_[ipdf]['alpha'],
-                    self._limits_sgn_pars_[ipdf]['alpha'][0], self._limits_sgn_pars_[ipdf]['alpha'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alpha'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigma'],
-                    self._limits_sgn_pars_[ipdf]['sigma'][0], self._limits_sgn_pars_[ipdf]['sigma'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigma'])
-                self._signal_pdf_[ipdf] = zfit.pdf.GeneralizedGaussExpTail(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigmar=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    sigmal=self._sgn_pars_[ipdf][f'{self._name_}_sigma_signal{ipdf}'],
-                    alphar=self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}'],
-                    alphal=self._sgn_pars_[ipdf][f'{self._name_}_alpha_signal{ipdf}']
-                )
-            elif pdf_name == 'bifurgaus':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigmal', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('sigmar', 0.010)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigmar', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigmal', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigmar', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigmal', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigmar_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigmar_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigmar'],
-                    self._limits_sgn_pars_[ipdf]['sigmar'][0], self._limits_sgn_pars_[ipdf]['sigmar'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigmar'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigmal_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigmal_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigmal'],
-                    self._limits_sgn_pars_[ipdf]['sigmal'][0], self._limits_sgn_pars_[ipdf]['sigmal'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigmal'])
-                self._signal_pdf_[ipdf] = zfit.pdf.BifurGauss(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigmar=self._sgn_pars_[ipdf][f'{self._name_}_sigmar_signal{ipdf}'],
-                    sigmal=self._sgn_pars_[ipdf][f'{self._name_}_sigmal_signal{ipdf}']
-                )
-            elif pdf_name == 'genercrystalball':
-                self._init_sgn_pars_[ipdf].setdefault('mu', 1.865)
-                self._init_sgn_pars_[ipdf].setdefault('sigmar', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('sigmal', 0.010)
-                self._init_sgn_pars_[ipdf].setdefault('alphal', 0.5)
-                self._init_sgn_pars_[ipdf].setdefault('nl', 1.)
-                self._init_sgn_pars_[ipdf].setdefault('alphar', 0.5)
-                self._init_sgn_pars_[ipdf].setdefault('nr', 1.)
-                self._fix_sgn_pars_[ipdf].setdefault('mu', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigmar', False)
-                self._fix_sgn_pars_[ipdf].setdefault('sigmal', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alphal', False)
-                self._fix_sgn_pars_[ipdf].setdefault('nl', False)
-                self._fix_sgn_pars_[ipdf].setdefault('alphar', False)
-                self._fix_sgn_pars_[ipdf].setdefault('nr', False)
-                self._limits_sgn_pars_[ipdf].setdefault('mu', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigmar', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('sigmal', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('alphal', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('nl', [0., None])
-                self._limits_sgn_pars_[ipdf].setdefault('alphar', [0, None])
-                self._limits_sgn_pars_[ipdf].setdefault('nr', [0., None])
-                self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mu_signal{ipdf}', self._init_sgn_pars_[ipdf]['mu'],
-                    self._limits_sgn_pars_[ipdf]['mu'][0], self._limits_sgn_pars_[ipdf]['mu'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['mu'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigmal_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigma_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigmal'],
-                    self._limits_sgn_pars_[ipdf]['sigmal'][0], self._limits_sgn_pars_[ipdf]['sigmal'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigmal'])
-                self._sgn_pars_[ipdf][f'{self._name_}_sigmar_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_sigmar_signal{ipdf}', self._init_sgn_pars_[ipdf]['sigmar'],
-                    self._limits_sgn_pars_[ipdf]['sigmar'][0], self._limits_sgn_pars_[ipdf]['sigmar'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['sigmar'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alphal_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alphal_signal{ipdf}', self._init_sgn_pars_[ipdf]['alphal'],
-                    self._limits_sgn_pars_[ipdf]['alphal'][0], self._limits_sgn_pars_[ipdf]['alphal'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alphal'])
-                self._sgn_pars_[ipdf][f'{self._name_}_nl_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_nl_signal{ipdf}', self._init_sgn_pars_[ipdf]['nl'],
-                    self._limits_sgn_pars_[ipdf]['nl'][0], self._limits_sgn_pars_[ipdf]['nl'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['nl'])
-                self._sgn_pars_[ipdf][f'{self._name_}_alphar_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_alphar_signal{ipdf}', self._init_sgn_pars_[ipdf]['alphar'],
-                    self._limits_sgn_pars_[ipdf]['alphar'][0], self._limits_sgn_pars_[ipdf]['alphar'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['alphar'])
-                self._sgn_pars_[ipdf][f'{self._name_}_nr_signal{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_nr_signal{ipdf}', self._init_sgn_pars_[ipdf]['nr'],
-                    self._limits_sgn_pars_[ipdf]['nr'][0], self._limits_sgn_pars_[ipdf]['nr'][1],
-                    floating=not self._fix_sgn_pars_[ipdf]['nr'])
-                self._signal_pdf_[ipdf] = zfit.pdf.GeneralizedCB(
-                    obs=obs,
-                    mu=self._sgn_pars_[ipdf][f'{self._name_}_mu_signal{ipdf}'],
-                    sigmar=self._sgn_pars_[ipdf][f'{self._name_}_sigmar_signal{ipdf}'],
-                    sigmal=self._sgn_pars_[ipdf][f'{self._name_}_sigmal_signal{ipdf}'],
-                    alphal=self._sgn_pars_[ipdf][f'{self._name_}_alphal_signal{ipdf}'],
-                    nl=self._sgn_pars_[ipdf][f'{self._name_}_nl_signal{ipdf}'],
-                    alphar=self._sgn_pars_[ipdf][f'{self._name_}_alphar_signal{ipdf}'],
-                    nr=self._sgn_pars_[ipdf][f'{self._name_}_nr_signal{ipdf}']
-                )
-            elif 'kde' in pdf_name:
-                if self._kde_signal_sample_[ipdf]:
-                    if pdf_name == 'kde_exact':
-                        self._signal_pdf_[ipdf] = zfit.pdf.KDE1DimExact(self._kde_signal_sample_[ipdf].get_data(),
-                                                                        obs=self._kde_signal_sample_[ipdf].get_obs(),
-                                                                        name=f'{self._name_}_kde_signal{ipdf}',
-                                                                        **self._kde_signal_option_[ipdf])
-                    elif pdf_name == 'kde_grid':
-                        self._signal_pdf_[ipdf] = zfit.pdf.KDE1DimGrid(self._kde_signal_sample_[ipdf].get_data(),
-                                                                       obs=self._kde_signal_sample_[ipdf].get_obs(),
-                                                                       name=f'{self._name_}_kde_signal{ipdf}',
-                                                                       **self._kde_signal_option_[ipdf])
-                    elif pdf_name == 'kde_fft':
-                        self._signal_pdf_[ipdf] = zfit.pdf.KDE1DimFFT(self._kde_signal_sample_[ipdf].get_data(),
-                                                                      obs=self._kde_signal_sample_[ipdf].get_obs(),
-                                                                      name=f'{self._name_}_kde_signal{ipdf}',
-                                                                      **self._kde_signal_option_[ipdf])
-                    elif pdf_name == 'kde_isj':
-                        self._signal_pdf_[ipdf] = zfit.pdf.KDE1DimISJ(self._kde_signal_sample_[ipdf].get_data(),
-                                                                      obs=self._kde_signal_sample_[ipdf].get_obs(),
-                                                                      name=f'{self._name_}_kde_signal{ipdf}',
-                                                                      **self._kde_signal_option_[ipdf])
-                else:
-                    Logger(f'Missing datasample for Kernel Density Estimation of signal {ipdf}!', 'FATAL')
             elif pdf_name == 'hist':
-                if self._hist_signal_sample_[ipdf]:
-                    self._signal_pdf_[ipdf] = zfit.pdf.SplinePDF(
-                        zfit.pdf.HistogramPDF(self._hist_signal_sample_[ipdf].get_binned_data(),
-                                              name=f'{self._name_}_hist_signal{ipdf}'),
-                        order=3,
-                        obs=obs
-                    )
-                else:
-                    Logger(f'Missing datasample for histogram template of signal {ipdf}!', 'FATAL')
+                self._signal_pdf_[ipdf] = PDFBuilder.build_signal_hist(
+                    obs,
+                    self._hist_signal_sample_[ipdf],
+                    self._name_,
+                    ipdf,
+                )
             else:
-                Logger(f'Signal pdf {pdf_name} not supported', 'FATAL')
+                self._signal_pdf_[ipdf], self._sgn_pars_[ipdf] = PDFBuilder.build_signal_pdf(
+                    pdf_name,
+                    obs,
+                    self._name_,
+                    ipdf,
+                    self._init_sgn_pars_[ipdf],
+                    self._limits_sgn_pars_[ipdf],
+                    self._fix_sgn_pars_[ipdf]
+                )
 
             if at_threshold:
                 # pion mass as default
@@ -754,165 +324,39 @@ def __build_background_pdfs(self, obs):
             if pdf_name == 'nobkg':
                 Logger('Performing fit with no bkg pdf', 'WARNING')
                 break
-            if pdf_name == 'expo':
-                self._init_bkg_pars_[ipdf].setdefault('lam', 0.1)
-                self._limits_bkg_pars_[ipdf].setdefault('lam', [None, None])
-                self._fix_bkg_pars_[ipdf].setdefault('lam', False)
-                self._bkg_pars_[ipdf][f'{self._name_}_lam_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_lam_bkg{ipdf}', self._init_bkg_pars_[ipdf]['lam'],
-                    self._limits_bkg_pars_[ipdf]['lam'][0], self._limits_bkg_pars_[ipdf]['lam'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['lam'])
-                self._background_pdf_[ipdf] = zfit.pdf.Exponential(
-                    obs=obs,
-                    lam=self._bkg_pars_[ipdf][f'{self._name_}_lam_bkg{ipdf}']
+            if 'kde' in pdf_name:
+                self._background_pdf_[ipdf] = PDFBuilder.build_bkg_kde(
+                    pdf_name,
+                    self._kde_bkg_sample_[ipdf],
+                    self._name_,
+                    ipdf,
+                    self._kde_bkg_option_[ipdf]
                 )
-            elif 'chebpol' in pdf_name:
-                pol_degree = int(pdf_name.split('chebpol')[1])
-                for deg in range(pol_degree + 1):
-                    self._init_bkg_pars_[ipdf].setdefault(f'c{deg}', 0.1)
-                    self._limits_bkg_pars_[ipdf].setdefault(f'c{deg}', [None, None])
-                    self._fix_bkg_pars_[ipdf].setdefault(f'c{deg}', False)
-                    self._bkg_pars_[ipdf][f'{self._name_}_c{deg}_bkg{ipdf}'] = zfit.Parameter(
-                        f'{self._name_}_c{deg}_bkg{ipdf}', self._init_bkg_pars_[ipdf][f'c{deg}'],
-                        self._limits_bkg_pars_[ipdf][f'c{deg}'][0], self._limits_bkg_pars_[ipdf][f'c{deg}'][1],
-                        floating=not self._fix_bkg_pars_[ipdf][f'c{deg}'])
-                coeff0 = self._bkg_pars_[ipdf][f'{self._name_}_c0_bkg{ipdf}']
-                bkg_coeffs = [self._bkg_pars_[ipdf][f'{self._name_}_c{deg}_bkg{ipdf}']
-                              for deg in range(1, pol_degree + 1)]
-                self._background_pdf_[ipdf] = zfit.pdf.Chebyshev(obs=obs, coeffs=bkg_coeffs, coeff0=coeff0)
-            elif 'powlaw' in pdf_name:
-                # pion mass as default
-                self._init_bkg_pars_[ipdf].setdefault('mass', self.pdg_api.get_particle_by_mcid(211).mass)
-                self._init_bkg_pars_[ipdf].setdefault('power', 1.)
-                self._limits_bkg_pars_[ipdf].setdefault('mass', [0., None])
-                self._limits_bkg_pars_[ipdf].setdefault('power', [None, None])
-                self._fix_bkg_pars_[ipdf].setdefault('mass', True)
-                self._fix_bkg_pars_[ipdf].setdefault('power', False)
-                if self._data_handler_.get_limits()[0] < self._init_bkg_pars_[ipdf]["mass"]:
-                    Logger('The mass parameter in powlaw cannot be smaller than the lower fit limit, please fix it.',
-                           'FATAL')
-                self._bkg_pars_[ipdf][f'{self._name_}_mass_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mass_bkg{ipdf}', self._init_bkg_pars_[ipdf]['mass'],
-                    self._limits_bkg_pars_[ipdf]['mass'][0], self._limits_bkg_pars_[ipdf]['mass'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['mass'])
-                self._bkg_pars_[ipdf][f'{self._name_}_power_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_power_bkg{ipdf}', self._init_bkg_pars_[ipdf]['power'],
-                    self._limits_bkg_pars_[ipdf]['power'][0], self._limits_bkg_pars_[ipdf]['power'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['power'])
-                self._background_pdf_[ipdf] = cpdf.Pow(
-                    obs=obs,
-                    mass=self._bkg_pars_[ipdf][f'{self._name_}_mass_bkg{ipdf}'],
-                    power=self._bkg_pars_[ipdf][f'{self._name_}_power_bkg{ipdf}']
-                )
-            elif 'expopowext' in pdf_name:
-                # pion mass as default
-                self._init_bkg_pars_[ipdf].setdefault('mass', self.pdg_api.get_particle_by_mcid(211).mass)
-                self._init_bkg_pars_[ipdf].setdefault('c1', -0.1)
-                self._init_bkg_pars_[ipdf].setdefault('c2', 0.)
-                self._init_bkg_pars_[ipdf].setdefault('c3', 0.)
-                self._init_bkg_pars_[ipdf].setdefault('power', 1./2)
-                self._limits_bkg_pars_[ipdf].setdefault('mass', [0., None])
-                self._limits_bkg_pars_[ipdf].setdefault('c1', [None, None])
-                self._limits_bkg_pars_[ipdf].setdefault('c2', [None, None])
-                self._limits_bkg_pars_[ipdf].setdefault('c3', [None, None])
-                self._limits_bkg_pars_[ipdf].setdefault('power', [None, None])
-                self._fix_bkg_pars_[ipdf].setdefault('mass', True)
-                self._fix_bkg_pars_[ipdf].setdefault('c1', False)
-                self._fix_bkg_pars_[ipdf].setdefault('c2', False)
-                self._fix_bkg_pars_[ipdf].setdefault('c3', False)
-                self._fix_bkg_pars_[ipdf].setdefault('power', False)
-                if self._data_handler_.get_limits()[0] < self._init_bkg_pars_[ipdf]["mass"]:
-                    Logger('The mass parameter in expopow cannot be smaller than the lower fit limit, please fix it.',
-                           'FATAL')
-                self._bkg_pars_[ipdf][f'{self._name_}_mass_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mass_bkg{ipdf}', self._init_bkg_pars_[ipdf]['mass'],
-                    self._limits_bkg_pars_[ipdf]['mass'][0], self._limits_bkg_pars_[ipdf]['mass'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['mass'])
-                self._bkg_pars_[ipdf][f'{self._name_}_power_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_power_bkg{ipdf}', self._init_bkg_pars_[ipdf]['power'],
-                    self._limits_bkg_pars_[ipdf]['power'][0], self._limits_bkg_pars_[ipdf]['power'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['power'])
-                self._bkg_pars_[ipdf][f'{self._name_}_c1_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_c1_bkg{ipdf}', self._init_bkg_pars_[ipdf]['c1'],
-                    self._limits_bkg_pars_[ipdf]['c1'][0], self._limits_bkg_pars_[ipdf]['c1'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['c1'])
-                self._bkg_pars_[ipdf][f'{self._name_}_c2_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_c2_bkg{ipdf}', self._init_bkg_pars_[ipdf]['c2'],
-                    self._limits_bkg_pars_[ipdf]['c2'][0], self._limits_bkg_pars_[ipdf]['c2'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['c2'])
-                self._bkg_pars_[ipdf][f'{self._name_}_c3_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_c3_bkg{ipdf}', self._init_bkg_pars_[ipdf]['c3'],
-                    self._limits_bkg_pars_[ipdf]['c3'][0], self._limits_bkg_pars_[ipdf]['c3'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['c3'])
-                self._background_pdf_[ipdf] = cpdf.ExpoPowExt(
-                    obs=obs,
-                    mass=self._bkg_pars_[ipdf][f'{self._name_}_mass_bkg{ipdf}'],
-                    power=self._bkg_pars_[ipdf][f'{self._name_}_power_bkg{ipdf}'],
-                    c1=self._bkg_pars_[ipdf][f'{self._name_}_c1_bkg{ipdf}'],
-                    c2=self._bkg_pars_[ipdf][f'{self._name_}_c2_bkg{ipdf}'],
-                    c3=self._bkg_pars_[ipdf][f'{self._name_}_c3_bkg{ipdf}']
-                )
-            elif 'expopow' in pdf_name:
-                # pion mass as default
-                self._init_bkg_pars_[ipdf].setdefault('mass', self.pdg_api.get_particle_by_mcid(211).mass)
-                self._init_bkg_pars_[ipdf].setdefault('lam', 0.1)
-                self._limits_bkg_pars_[ipdf].setdefault('mass', [0., None])
-                self._limits_bkg_pars_[ipdf].setdefault('lam', [None, None])
-                self._fix_bkg_pars_[ipdf].setdefault('mass', True)
-                self._fix_bkg_pars_[ipdf].setdefault('lam', False)
-                if self._data_handler_.get_limits()[0] < self._init_bkg_pars_[ipdf]["mass"]:
-                    Logger('The mass parameter in expopow cannot be smaller than the lower fit limit, please fix it.',
-                           'FATAL')
-                self._bkg_pars_[ipdf][f'{self._name_}_mass_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_mass_bkg{ipdf}', self._init_bkg_pars_[ipdf]['mass'],
-                    self._limits_bkg_pars_[ipdf]['mass'][0], self._limits_bkg_pars_[ipdf]['mass'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['mass'])
-                self._bkg_pars_[ipdf][f'{self._name_}_lam_bkg{ipdf}'] = zfit.Parameter(
-                    f'{self._name_}_lam_bkg{ipdf}', self._init_bkg_pars_[ipdf]['lam'],
-                    self._limits_bkg_pars_[ipdf]['lam'][0], self._limits_bkg_pars_[ipdf]['lam'][1],
-                    floating=not self._fix_bkg_pars_[ipdf]['lam'])
-                self._background_pdf_[ipdf] = cpdf.ExpoPow(
-                    obs=obs,
-                    mass=self._bkg_pars_[ipdf][f'{self._name_}_mass_bkg{ipdf}'],
-                    lam=self._bkg_pars_[ipdf][f'{self._name_}_lam_bkg{ipdf}']
-                )
-            elif 'kde' in pdf_name:
-                if self._kde_bkg_sample_[ipdf]:
-                    if pdf_name == 'kde_exact':
-                        self._background_pdf_[ipdf] = zfit.pdf.KDE1DimExact(self._kde_bkg_sample_[ipdf].get_data(),
-                                                                            obs=self._kde_bkg_sample_[ipdf].get_obs(),
-                                                                            name=f'{self._name_}_kde_bkg{ipdf}',
-                                                                            **self._kde_bkg_option_[ipdf])
-                    elif pdf_name == 'kde_grid':
-                        self._background_pdf_[ipdf] = zfit.pdf.KDE1DimGrid(self._kde_bkg_sample_[ipdf].get_data(),
-                                                                           obs=self._kde_bkg_sample_[ipdf].get_obs(),
-                                                                           name=f'{self._name_}_kde_bkg{ipdf}',
-                                                                           **self._kde_bkg_option_[ipdf])
-                    elif pdf_name == 'kde_fft':
-                        self._background_pdf_[ipdf] = zfit.pdf.KDE1DimFFT(self._kde_bkg_sample_[ipdf].get_data(),
-                                                                          obs=self._kde_bkg_sample_[ipdf].get_obs(),
-                                                                          name=f'{self._name_}_kde_bkg{ipdf}',
-                                                                          **self._kde_bkg_option_[ipdf])
-                    elif pdf_name == 'kde_isj':
-                        self._background_pdf_[ipdf] = zfit.pdf.KDE1DimISJ(self._kde_bkg_sample_[ipdf].get_data(),
-                                                                          obs=self._kde_bkg_sample_[ipdf].get_obs(),
-                                                                          name=f'{self._name_}_kde_bkg{ipdf}',
-                                                                          **self._kde_bkg_option_[ipdf])
 
-                else:
-                    Logger(f'Missing datasample for Kernel Density Estimation of background {ipdf}!', 'FATAL')
             elif pdf_name == 'hist':
-                if self._hist_bkg_sample_[ipdf]:
-                    self._background_pdf_[ipdf] = zfit.pdf.SplinePDF(
-                        zfit.pdf.HistogramPDF(self._hist_bkg_sample_[ipdf].get_binned_data(),
-                                              name=f'{self._name_}_hist_background{ipdf}'),
-                        order=3,
-                        obs=obs
-                    )
-                else:
-                    Logger(f'Missing datasample for histogram template of background {ipdf}!', 'FATAL')
+                self._background_pdf_[ipdf] = PDFBuilder.build_bkg_hist(
+                    obs,
+                    self._hist_bkg_sample_[ipdf],
+                    self._name_,
+                    ipdf,
+                )
             else:
-                Logger(f'Background pdf {pdf_name} not supported', 'FATAL')
+                self._background_pdf_[ipdf], self._bkg_pars_[ipdf] = PDFBuilder.build_bkg_pdf(
+                    pdf_name,
+                    obs,
+                    self._name_,
+                    ipdf,
+                    self._init_bkg_pars_[ipdf],
+                    self._limits_bkg_pars_[ipdf],
+                    self._fix_bkg_pars_[ipdf]
+                )
+                if pdf_name in ['powlaw', 'expopow', 'expopowext'] and\
+                        self._data_handler_.get_limits()[0] < self._init_bkg_pars_[ipdf]["mass"]:
+                    Logger(
+                        'The mass parameter in powlaw cannot be smaller than the lower fit limit, '
+                        'please fix it.',
+                        'FATAL'
+                    )
 
     # pylint: disable=too-many-branches, too-many-statements
     def __build_reflection_pdfs(self, obs):
diff --git a/flarefly/pdf_builder.py b/flarefly/pdf_builder.py
new file mode 100644
index 0000000..3f414a0
--- /dev/null
+++ b/flarefly/pdf_builder.py
@@ -0,0 +1,284 @@
+"""PDFBuilder class to create signal and background PDFs using zfit."""
+from typing import Dict, Any, List, Optional, Tuple
+import zfit
+from flarefly.pdf_configs import get_signal_pdf_config, get_bkg_pdf_config, get_kde_pdf
+from flarefly.utils import Logger
+
+
+class PDFBuilder:
+    """Class to build signal and background PDFs using zfit."""
+    @staticmethod
+    def build_signal_pdf(
+        pdf_name: str,
+        obs: zfit.Space,
+        name: str,
+        ipdf: int,
+        init_pars: Dict[str, float],
+        limits_pars: Dict[str, List[float]],
+        fix_pars: Dict[str, bool]
+    ) -> zfit.pdf.BasePDF:
+        """Build a signal PDF with configurable parameters.
+
+        Args:
+            pdf_name: Name of the PDF to build
+            obs: The observable space for the PDF
+            name: Base name for parameters
+            ipdf: Index of the PDF
+            init_pars: Dictionary that will be updated with default initial values
+            limits_pars: Dictionary that will be updated with default limits
+            fix_pars: Dictionary that will be updated with default fix flags
+
+        Returns:
+            - The constructed zfit PDF object
+            - Dictionary of zfit.Parameter objects for the PDF parameters
+        """
+        config = get_signal_pdf_config(pdf_name)
+
+        # Update the input dictionaries with default values
+        PDFBuilder._update_with_defaults(config, init_pars, limits_pars, fix_pars)
+
+        parameters = {}
+        for par_name in config['parameters'].keys():
+            param_name = f'{name}_{par_name}_signal{ipdf}'
+            parameters[param_name] = zfit.Parameter(
+                name=param_name,
+                value=init_pars[par_name],
+                lower=limits_pars[par_name][0],
+                upper=limits_pars[par_name][1],
+                floating=not fix_pars[par_name]
+            )
+
+        pdf_args = {'obs': obs}
+        for arg_name in config['pdf_args']:
+            if 'args_mapping' in config:
+                param_key = f"{name}_{config['args_mapping'][arg_name]}_signal{ipdf}"
+            else:
+                param_key = f'{name}_{arg_name}_signal{ipdf}'
+            pdf_args[arg_name] = parameters[param_key]
+
+        return config['pdf_class'](**pdf_args), parameters
+
+    @staticmethod
+    def build_signal_kde(
+        pdf_name: str,
+        kde_sample: Any,
+        name: str,
+        ipdf: int,
+        kde_options: Optional[Dict[str, Any]] = None
+    ) -> zfit.pdf.BasePDF:
+        """Build a signal KDE PDF.
+
+        Args:
+            pdf_name: Type of KDE ('kde_exact', 'kde_grid', 'kde_fft', 'kde_isj')
+            kde_sample: The sample data for KDE estimation
+            name: Base name for the PDF
+            ipdf: Index of the PDF
+            kde_options: Additional options for the KDE
+
+        Returns:
+            The constructed KDE PDF
+        """
+        if not kde_sample:
+            Logger(f'Missing datasample for Kernel Density Estimation of signal {ipdf}!', 'FATAL')
+
+        kde_options = kde_options or {}
+
+        return get_kde_pdf(pdf_name)(
+            data=kde_sample.get_data(),
+            obs=kde_sample.get_obs(),
+            name=f'{name}_kde_signal{ipdf}',
+            **kde_options
+        )
+
+    @staticmethod
+    def build_signal_hist(
+        obs: zfit.Space,
+        hist_signal_sample: Any,
+        name: str,
+        ipdf: int,
+    ) -> zfit.pdf.BasePDF:
+        """Build a signal PDF from a histogram template.
+
+        Args:
+            hist_signal_sample: The histogram
+            name: Base name for the PDF
+            ipdf: Index of the PDF
+
+        Returns:
+            The constructed PDF
+        """
+        if not hist_signal_sample:
+            Logger(f'Missing datasample for histogram template of signal {ipdf}!', 'FATAL')
+
+        return zfit.pdf.SplinePDF(
+            zfit.pdf.HistogramPDF(
+                hist_signal_sample.get_binned_data(),
+                name=f'{name}_hist_signal{ipdf}'
+            ),
+            order=3,
+            obs=obs
+        )
+
+    @staticmethod
+    def build_bkg_pdf(
+        pdf_name: str,
+        obs: zfit.Space,
+        name: str,
+        ipdf: int,
+        init_pars: Dict[str, float],
+        limits_pars: Dict[str, List[float]],
+        fix_pars: Dict[str, bool]
+    ) -> zfit.pdf.BasePDF:
+        """Build a signal PDF with configurable parameters.
+
+        Args:
+            pdf_name: Name of the PDF to build
+            obs: The observable space for the PDF
+            name: Base name for parameters
+            ipdf: Index of the PDF
+            init_pars: Dictionary that will be updated with default initial values
+            limits_pars: Dictionary that will be updated with default limits
+            fix_pars: Dictionary that will be updated with default fix flags
+
+        Returns:
+            - The constructed zfit PDF object
+            - Dictionary of zfit.Parameter objects for the PDF parameters
+        """
+        # Handle Chebyshev polynomials specially
+        if 'chebpol' in pdf_name:
+            return PDFBuilder._build_chebyshev_pdf(
+                pdf_name, obs, name, ipdf, init_pars, limits_pars, fix_pars
+            )
+
+        config = get_bkg_pdf_config(pdf_name)
+
+        # Update the input dictionaries with default values
+        PDFBuilder._update_with_defaults(config, init_pars, limits_pars, fix_pars)
+
+        parameters = {}
+        for par_name in config['parameters'].keys():
+            param_name = f'{name}_{par_name}_bkg{ipdf}'
+            parameters[param_name] = zfit.Parameter(
+                name=param_name,
+                value=init_pars[par_name],
+                lower=limits_pars[par_name][0],
+                upper=limits_pars[par_name][1],
+                floating=not fix_pars[par_name]
+            )
+
+        pdf_args = {'obs': obs}
+        for arg_name in config['pdf_args']:
+            param_key = f'{name}_{arg_name}_bkg{ipdf}'
+            pdf_args[arg_name] = parameters[param_key]
+
+        return config['pdf_class'](**pdf_args), parameters
+
+    @staticmethod
+    def _build_chebyshev_pdf(
+        pdf_name: str,
+        obs: zfit.Space,
+        name: str,
+        ipdf: int,
+        init_pars: Dict[str, float],
+        limits_pars: Dict[str, List[float]],
+        fix_pars: Dict[str, bool]
+    ) -> Tuple[zfit.pdf.BasePDF, Dict[str, zfit.Parameter]]:
+        """Build a Chebyshev polynomial background PDF."""
+        pol_degree = int(pdf_name.split('chebpol')[1])
+
+        # Create parameters for each coefficient
+        parameters = {}
+        for deg in range(pol_degree + 1):
+            par_name = f'c{deg}'
+            init_pars.setdefault(par_name, 0.1)
+            limits_pars.setdefault(par_name, [None, None])
+            fix_pars.setdefault(par_name, False)
+
+            param_name = f'{name}_{par_name}_bkg{ipdf}'
+            parameters[param_name] = zfit.Parameter(
+                name=param_name,
+                value=init_pars[par_name],
+                lower=limits_pars[par_name][0],
+                upper=limits_pars[par_name][1],
+                floating=not fix_pars[par_name]
+            )
+
+        # Prepare Chebyshev arguments
+        coeff0 = parameters[f'{name}_c0_bkg{ipdf}']
+        bkg_coeffs = [parameters[f'{name}_c{deg}_bkg{ipdf}'] for deg in range(1, pol_degree + 1)]
+
+        return zfit.pdf.Chebyshev(obs=obs, coeff0=coeff0, coeffs=bkg_coeffs), parameters
+
+    @staticmethod
+    def build_bkg_kde(
+        pdf_name: str,
+        kde_sample: Any,
+        name: str,
+        ipdf: int,
+        kde_options: Optional[Dict[str, Any]] = None
+    ) -> zfit.pdf.BasePDF:
+        """Build a background KDE PDF.
+
+        Args:
+            pdf_name: Type of KDE ('kde_exact', 'kde_grid', 'kde_fft', 'kde_isj')
+            kde_sample: The sample data for KDE estimation
+            name: Base name for the PDF
+            ipdf: Index of the PDF
+            kde_options: Additional options for the KDE
+
+        Returns:
+            The constructed KDE PDF
+        """
+        if not kde_sample:
+            Logger(f'Missing datasample for Kernel Density Estimation of background {ipdf}!', 'FATAL')
+
+        kde_options = kde_options or {}
+
+        return get_kde_pdf(pdf_name)(
+            data=kde_sample.get_data(),
+            obs=kde_sample.get_obs(),
+            name=f'{name}_kde_bkg{ipdf}',
+            **kde_options
+        )
+
+    @staticmethod
+    def build_bkg_hist(
+        obs: zfit.Space,
+        hist_bkg_sample: Any,
+        name: str,
+        ipdf: int,
+    ) -> zfit.pdf.BasePDF:
+        """Build a bkg PDF from a histogram template.
+
+        Args:
+            hist_bkg_sample: The histogram
+            name: Base name for the PDF
+            ipdf: Index of the PDF
+
+        Returns:
+            The constructed PDF
+        """
+        if not hist_bkg_sample:
+            Logger(f'Missing datasample for histogram template of background {ipdf}!', 'FATAL')
+
+        return zfit.pdf.SplinePDF(
+            zfit.pdf.HistogramPDF(
+                hist_bkg_sample.get_binned_data(),
+                name=f'{name}_hist_bkg{ipdf}'
+            ),
+            order=3,
+            obs=obs
+        )
+
+    @staticmethod
+    def _update_with_defaults(
+        config: Dict[str, Any],
+        init_pars: Dict[str, float],
+        limits_pars: Dict[str, List[float]],
+        fix_pars: Dict[str, bool]
+    ) -> None:
+        """Update the parameter dictionaries with default values from config."""
+        for par_name, par_config in config['parameters'].items():
+            init_pars.setdefault(par_name, par_config['init'])
+            limits_pars.setdefault(par_name, par_config['limits'])
+            fix_pars.setdefault(par_name, par_config['fix'])
diff --git a/flarefly/pdf_configs.py b/flarefly/pdf_configs.py
new file mode 100644
index 0000000..05941c7
--- /dev/null
+++ b/flarefly/pdf_configs.py
@@ -0,0 +1,456 @@
+"""Helper functions to get the PDF configurations for signal and background PDFs."""
+import zfit
+import pdg
+import flarefly.custom_pdfs as cpdf
+from flarefly.utils import Logger
+
+pdg_api = pdg.connect()
+
+SIGNAL_PDF_CONFIGS = {
+    'gaussian': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.Gauss,
+        'pdf_args': ['mu', 'sigma']
+    },
+    'doublegaus': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma1': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma2': {
+                'init': 0.100,
+                'limits': [0., None],
+                'fix': False
+            },
+            'frac1': {
+                'init': 0.9,
+                'limits': [0., 1.],
+                'fix': False
+            }
+        },
+        'pdf_class': cpdf.DoubleGauss,
+        'pdf_args': ['mu', 'sigma1', 'sigma2', 'frac1']
+    },
+    'crystalball': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alpha': {
+                'init': 0.5,
+                'limits': [0, None],
+                'fix': False
+            },
+            'n': {
+                'init': 1.,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.CrystalBall,
+        'pdf_args': ['mu', 'sigma', 'alpha', 'n']
+    },
+    'doublecb': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alphal': {
+                'init': 0.5,
+                'limits': [0, None],
+                'fix': False
+            },
+            'nl': {
+                'init': 1.,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alphar': {
+                'init': 0.5,
+                'limits': [0, None],
+                'fix': False
+            },
+            'nr': {
+                'init': 1.,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.DoubleCB,
+        'pdf_args': ['mu', 'sigma', 'alphal', 'nl', 'alphar', 'nr']
+    },
+    'doublecbsymm': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alpha': {
+                'init': 0.5,
+                'limits': [0, None],
+                'fix': False
+            },
+            'n': {
+                'init': 1.,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'args_mapping': {
+            'mu': 'mu',
+            'sigma': 'sigma',
+            'nl': 'n',
+            'nr': 'n',
+            'alphar': 'alpha',
+            'alphal': 'alpha'
+        },
+        'pdf_class': zfit.pdf.DoubleCB,
+        'pdf_args': ['mu', 'sigma', 'alphal', 'nl', 'alphar', 'nr']
+    },
+    'cauchy': {
+        'parameters': {
+            'm': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'gamma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.Cauchy,
+        'pdf_args': ['m', 'gamma']
+    },
+    'voigtian': {
+        'parameters': {
+            'm': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'gamma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.Voigt,
+        'pdf_args': ['m', 'sigma', 'gamma']
+    },
+    'gausexptail': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alpha': {
+                'init': 1.e6,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.GaussExpTail,
+        'pdf_args': ['mu', 'sigma', 'alpha']
+    },
+    'genergausexptail': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigmal': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigmar': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alphal': {
+                'init': 1.e6,
+                'limits': [None, None],
+                'fix': False
+            },
+            'alphar': {
+                'init': 1.e6,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.GeneralizedGaussExpTail,
+        'pdf_args': ['mu', 'sigmar', 'sigmal', 'alphar', 'alphal']
+    },
+    'genergausexptailsymm': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigma': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alpha': {
+                'init': 1.e6,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'args_mapping': {
+            'mu': 'mu',
+            'sigmar': 'sigma',
+            'sigmal': 'sigma',
+            'alphar': 'alpha',
+            'alphal': 'alpha'
+        },
+        'pdf_class': zfit.pdf.GeneralizedGaussExpTail,
+        'pdf_args': ['mu', 'sigmar', 'sigmal', 'alphar', 'alphal']
+    },
+    'bifurgaus': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigmal': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigmar': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.BifurGauss,
+        'pdf_args': ['mu', 'sigmar', 'sigmal']
+    },
+    'genercrystalball': {
+        'parameters': {
+            'mu': {
+                'init': 1.865,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigmal': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'sigmar': {
+                'init': 0.010,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alphal': {
+                'init': 0.5,
+                'limits': [0, None],
+                'fix': False
+            },
+            'nl': {
+                'init': 1.,
+                'limits': [0., None],
+                'fix': False
+            },
+            'alphar': {
+                'init': 0.5,
+                'limits': [0, None],
+                'fix': False
+            },
+            'nr': {
+                'init': 1.,
+                'limits': [0., None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.GeneralizedCB,
+        'pdf_args': ['mu', 'sigmar', 'sigmal', 'alphal', 'nl', 'alphar', 'nr']
+    }
+}
+
+BACKGROUND_PDF_CONFIGS = {
+    'chebpol': {
+        'parameters': {
+            'c': {
+                'init': 0.1,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.Exponential,
+        'pdf_args': ['coeff0', 'coeffs']
+    },
+    'expo': {
+        'parameters': {
+            'lam': {
+                'init': 0.1,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': zfit.pdf.Exponential,
+        'pdf_args': ['lam']
+    },
+    'powlaw': {
+        'parameters': {
+            'mass': {
+                'init': pdg_api.get_particle_by_mcid(211).mass,  # pion mass
+                'limits': [0., None],
+                'fix': True
+            },
+            'power': {
+                'init': 1.,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': cpdf.Pow,
+        'pdf_args': ['mass', 'power']
+    },
+    'expopow': {
+        'parameters': {
+            'mass': {
+                'init': pdg_api.get_particle_by_mcid(211).mass,  # pion mass
+                'limits': [0., None],
+                'fix': True
+            },
+            'lam': {
+                'init': 0.1,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': cpdf.ExpoPow,
+        'pdf_args': ['mass', 'lam']
+    },
+    'expopowext': {
+        'parameters': {
+            'mass': {
+                'init': pdg_api.get_particle_by_mcid(211).mass,  # pion mass
+                'limits': [0., None],
+                'fix': True
+            },
+            'power': {
+                'init': 0.5,
+                'limits': [0., None],
+                'fix': False
+            },
+            'c1': {
+                'init': -0.1,
+                'limits': [None, None],
+                'fix': False
+            },
+            'c2': {
+                'init': 0.,
+                'limits': [None, None],
+                'fix': False
+            },
+            'c3': {
+                'init': 0.,
+                'limits': [None, None],
+                'fix': False
+            }
+        },
+        'pdf_class': cpdf.ExpoPowExt,
+        'pdf_args': ['mass', 'power', 'c1', 'c2', 'c3']
+    }
+}
+
+KDE_MAP = {
+    'kde_exact': zfit.pdf.KDE1DimExact,
+    'kde_grid': zfit.pdf.KDE1DimGrid,
+    'kde_fft': zfit.pdf.KDE1DimFFT,
+    'kde_isj': zfit.pdf.KDE1DimISJ
+}
+
+
+def get_signal_pdf_config(pdf_name: str):
+    """Get the configuration for a specific PDF."""
+    if pdf_name not in SIGNAL_PDF_CONFIGS:
+        Logger(f"Unknown signal PDF name: {pdf_name}.", "FATAL")
+
+    # Get the specific config
+    config = SIGNAL_PDF_CONFIGS[pdf_name].copy()
+
+    return config
+
+
+def get_bkg_pdf_config(pdf_name: str):
+    """Get the configuration for a specific PDF."""
+    if pdf_name not in BACKGROUND_PDF_CONFIGS:
+        Logger(f"Unknown background PDF name: {pdf_name}.", "FATAL")
+
+    # Get the specific config
+    config = BACKGROUND_PDF_CONFIGS[pdf_name].copy()
+
+    return config
+
+
+def get_kde_pdf(kde_name: str):
+    """Get the KDE PDF class for a specific KDE type."""
+    if kde_name not in KDE_MAP:
+        Logger(
+            f"Unknown KDE type: {kde_name}. "
+            f"Available types: {list(KDE_MAP.keys())}",
+            "FATAL"
+        )
+
+    return KDE_MAP[kde_name]


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.




state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T13:24:47Z
user: login: fgrosa
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T07:50:12Z
user: login: fgrosa
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T07:52:47Z
user: login: fgrosa
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:17:23Z

Review comments:
diff_hunk: @@ -0,0 +1,284 @@
+"""PDFBuilder class to create signal and background PDFs using zfit."""
+from typing import Dict, Any, List, Optional, Tuple
+import zfit
+from flarefly.pdf_configs import get_signal_pdf_config, get_bkg_pdf_config, get_kde_pdf
+from flarefly.utils import Logger
+
+
+class PDFBuilder:
+    """Class to build signal and background PDFs using zfit."""
+    @staticmethod
+    def build_signal_pdf(
+        pdf_name: str,
+        obs: zfit.Space,
+        name: str,
+        ipdf: int,
+        init_pars: Dict[str, float],
+        limits_pars: Dict[str, List[float]],
+        fix_pars: Dict[str, bool]
+    ) -> zfit.pdf.BasePDF:
+        """Build a signal PDF with configurable parameters.
+
+        Args:
+            pdf_name: Name of the PDF to build
+            obs: The observable space for the PDF
+            name: Base name for parameters
+            ipdf: Index of the PDF
+            init_pars: Dictionary that will be updated with default initial values
+            limits_pars: Dictionary that will be updated with default limits
+            fix_pars: Dictionary that will be updated with default fix flags
+
+        Returns:
+            - The constructed zfit PDF object
+            - Dictionary of zfit.Parameter objects for the PDF parameters
+        """
+        config = get_signal_pdf_config(pdf_name)
+
+        # Update the input dictionaries with default values
+        PDFBuilder._update_with_defaults(config, init_pars, limits_pars, fix_pars)
+
+        parameters = {}
+        for par_name in config['parameters'].keys():
+            param_name = f'{name}_{par_name}_signal{ipdf}'
+            parameters[param_name] = zfit.Parameter(
+                name=param_name,
+                value=init_pars[par_name],
+                lower=limits_pars[par_name][0],
+                upper=limits_pars[par_name][1],
+                floating=not fix_pars[par_name]
+            )
+
+        pdf_args = {'obs': obs}
+        for arg_name in config['pdf_args']:
+            if 'args_mapping' in config:
+                param_key = f'{name}_{config['args_mapping'][arg_name]}_signal{ipdf}'
path: flarefly/pdf_builder.py
user: login: Copilot
body: The nested single quotes in the f-string on this line can cause a syntax error. Consider using double quotes for the outer string, for example: param_key = f"{name}_{config['args_mapping'][arg_name]}_signal{ipdf}".
```suggestion
                param_key = f"{name}_{config['args_mapping'][arg_name]}_signal{ipdf}"
```
created_at: 2025-04-17T13:24:46Z
author_association: NONE
diff_hunk: @@ -88,6 +88,8 @@
             FITTERBINNEDDSTAR[-1].set_signal_initpar(0, "sigma", 0.0007, limits=[0.0001, 0.0015])
         elif sgn_pdf == "voigtian":
             FITTERBINNEDDSTAR[-1].set_signal_initpar(0, "gamma", 70.e-6)  # 70 keV
+        if bkg_pdf == "powlaw":
path: tests/test_massfitter_binned.py
user: login: fgrosa
body: ```suggestion
        if bkg_pdf in ["powlaw", "expopow", "expopowext"]:
```
created_at: 2025-04-22T07:52:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: fgrosa
created_at: 2025-04-22T07:51:50Z
body: > With this PR the code for the creation of the PDF has been refactored. This should simplify the support for additional PDFs, and also the implementation of the possibility to fit using ROOFit. Please let me know if this change can fit in the project. When writing this code, I realised that the `mass` parameter of the `powlaw` background function is set to fixed by default, is this expected? If yes, I can make slight changes to the `pdf_config.py` file to make it the only parameter fixed by default.

Hi @fchinu thanks! It looks very good to me, especially because it was becoming very large!
Regarding the `mass` parameter in `powlaw`, the idea is indeed to have it fixed to the pion mass as default (since it is mostly used for the D*). It should be fixed also for `expopow` and `expopowext` however
author_association: CONTRIBUTOR
user: login: fchinu
created_at: 2025-04-22T09:26:44Z
body: Hi @fgrosa, thanks a lot for your suggestion! I took a different approach as compared to your requested changes, and explicitly set a `fix` value for the different parameters. Now there is no need to put an `if` in the test to fix the `mass` parameters for the `powlaw`, `expopow` and `expopowext`. Let me know if this is fine!

author_association: CONTRIBUTOR