from __future__ import division
import logging
from typing import Literal, Union
import cmath # noqa: F401
from cmath import pi, sin, cos, tan, atan, sqrt, log # noqa: F401
from anyBSM.ufo.function_library import complexconjugate, conjugate, Abs, I, Re, Im # noqa: F401
from anyBSM.anyH3 import anyH3
from anyBSM.utils import lazy_import
from anyBSM.physic_utils import RunAlfas, tplus, tminus # noqa: F401
from anyBSM.integrators import anyIntegrator
from anyBSM.locks import CreateLock, safe_open, safe_isfile
from tqdm.auto import tqdm
import anyBSM.formfactors as formfactors
from hashlib import md5
import numpy as np
from scipy.interpolate import interp1d
from pandas import DataFrame, read_hdf, MultiIndex
from os import path, listdir
import sympy as sp
import json
import inspect

try:
    import lhapdf
    lhapdf.setVerbosity(0)
except ModuleNotFoundError:
    lhapdf = None

logging.basicConfig(format='%(levelname)s:%(message)s')
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

ff = formfactors.diHiggs(evaluation='analytical')

quark_pdgs = {1: 'd', 2: 'u', 3: 's', 4: 'c', 5: 'b'}
partons = {f'{q}-{q}bar': [i,-i] for i,q in quark_pdgs.items()}
""" contains all parton combinations that go into luminosities """
partons.update({f'g-{q}': [21,i] for i,q in quark_pdgs.items()})
partons.update({f'g-{q}bar': [21,-i] for i,q in quark_pdgs.items()})
partons.update({'g-g': [21,21]})

integrate = anyIntegrator('vegas')

def sighat_NLO_real_gg(tau,taumin,s,sighat_LO,scalefactor = 1,NF=5,**params):
    Pgg_delta, Pgg_rest, plus_rest = 0, 0, 0

    Shat = s*tau
    log1 = log(scalefactor**2).real
    log_rest = log(1-taumin/tau).real

    fac1 = RunAlfas(params['MZ'], scalefactor*sqrt(Shat).real, params['aS'])/pi
    siglo1 = fac1*sighat_LO(Shat).real

    if abs(log1) > 1e-11:
        Pgg_delta = -(33-2*NF)/6.*log1*siglo1
        Pgg_rest = -6*siglo1*fac1*log_rest*log1
    if abs(log_rest) > 1e-11:
        plus_rest = -6*siglo1*fac1*log_rest**2

    def integrand(z):
        facz = RunAlfas(params['MZ'], scalefactor*sqrt(z*Shat).real, params['aS'])/pi
        sigloz = facz*sighat_LO(Shat*z).real
        logz = log(z*scalefactor**2).real

        regular = -11/2.*(1-z)**3/z*sigloz
        Pgg_regular = -6*(1/z - 2 + z*(1-z))*logz*sigloz
        Pgg_plus = -6*(sigloz*logz - siglo1*log1)/(1-z)
        plus = 6*(sigloz*(1+z**4+(1-z)**4)/z - 2*siglo1)*log(1-z)/(1-z)
        return regular + Pgg_regular + Pgg_plus + plus.real

    integral = integrate(integrand, [[max(taumin/tau,1e-8), 1-1e-8]])
    return integral[0] + Pgg_delta + plus_rest + Pgg_rest

def sighat_NLO_real_gq(tau,taumin,s,sighat_LO,scalefactor = 1,NF=5,**params):
    Shat = s*tau

    def integrand(z):
        fac = RunAlfas(params['MZ'], scalefactor*sqrt(z*Shat).real, params['aS'])/pi
        sigloz = sighat_LO(z*Shat).real
        logz = log(z*scalefactor**2/(1-z)**2).real
        Pgq = -2/3.*(1 + (1 - z)**2)*logz
        return fac*(Pgq + 2*z**2/3. - (1-z)**2)*sigloz/z

    return integrate(integrand, [[max(taumin/tau,1e-8), 1-1e-8]])[0]

def sighat_NLO_real_qq(tau,taumin,s,sighat_LO,scalefactor = 1,NF=5, **params):
    Shat = s*tau

    def integrand(z):
        fac = RunAlfas(params['MZ'], scalefactor*sqrt(z*Shat).real, params['aS'])/pi
        return fac*32/27.*(1-z)**3*sighat_LO(z*Shat).real

    return integrate(integrand, [[max(taumin/tau,1e-8), 1-1e-8]])[0]

def luminosity(p, tau, x, q,f1,f2):
    sym = 2. if f1 == f2 else 1.
    return 1./sym/x*(p.xfxQ(f1, x, q) * p.xfxQ(f2, tau/x, q)
                   + p.xfxQ(f2, x, q) * p.xfxQ(f1, tau/x, q))/tau

def integrate_luminosities(pdf_set,scale,luminosities_file,sqrtS=14000):
    """ This function determines the gg/gq/qq luminosities
     Args:
        * pdf_set: the PDF set to use
        * scale: the scale at which the integration of the PDFs is performed
        * luminosities_file: save the obtained DataFrame grid to a h5 file
     Returns:
        A `DataFrame` containing the luminosities.
    """
    p = lhapdf.mkPDF(pdf_set, 0)
    taumin = np.floor(np.log10(scale**2/sqrtS**2))
    taus = np.logspace(taumin, 0, 5000)

    luminosities = DataFrame({'tau': taus})
    luminosities.set_index('tau', inplace=True)
    luminosities.columns = MultiIndex.from_product(
            [[], [],[], []],
            names=['pdf_set', 'scale', 'flavor', 'result'])
    print(f'integrating PDFs at fixed scale {scale} to obtain parton luminosities (has do be done only once):')
    for label,ps in partons.items():
        print(label)
        integral = [integrate(lambda x: luminosity(p, tau, x, scale, ps[0], ps[1]), [[tau, 1]], integrator='nquad')
                    for tau in luminosities.index]
        integral = np.array([list(x) for x in integral])
        luminosities[pdf_set, scale, label, 'value'] = integral[:, 0]
        luminosities[pdf_set, scale, label, 'error'] = integral[:, 1]
    logger.info(f'saving luminosities to {luminosities_file}')
    luminosities.to_hdf(luminosities_file, key='luminosities', mode='w')
    return luminosities

def et(s):
    """ escape tilde (~) in field names """
    return s.replace('~','tilde')

class anyHH(anyH3):
    # TODO: check that Gbox and Fbox are symmetric under MHa <-> MHb
    def load_PDF(
                self,
                pdf_scale : Union[float,str] = 250,
                pdf_set : str = 'PDF4LHC15_nlo_mc') -> None:
        """
        Loads the PDF (Parton Distribution Function) for the calculation of luminosities.

        Args:
            pdf_scale (float): The scale at which the PDF is evaluated. Default is 250. Alternatively,
                the string 'mhh' can be used to indicate that the dynamical scale should be used.
            pdf_set (str): The name of the PDF set to be used. Default is 'PDF4LHC15_nlo_mc'.

        Returns:
            None
        """

        self.lumi = {}
        self.ggHH['pdf_scale'] = pdf_scale

        if pdf_scale == 'mhh':
            if not lhapdf:
                logger.error('LHAPDF not found. Cannot chose dynamical scale. Trying to fall-back to pre-computed one (PDF4LHC15_nlo_mc@250GeV).')
                pdf_scale = 250
                pdf_set = 'PDF4LHC15_nlo_mc'
            else:
                if hasattr(self,'pdf') and self.pdf.set().name == pdf_set:
                    return
                self.pdf = lhapdf.mkPDF(pdf_set, 0)
                for label,p in partons.items():
                    self.lumi[label] = lambda tau, q: integrate(lambda x: luminosity(self.pdf, tau, x, q,p[0],p[1]), [[tau, 1]], integrator='nquad')[0]
                    self.lumi[label] = self.lumi[label]
                self.lumi['q-qbar'] = lambda x : sum(self.lumi[f'{q}-{q}bar'](x) for q in ['u','d','c','s','b'])
                self.lumi['g-q'] = lambda x : sum([self.lumi[label](x) for label in partons if label.count('g') == 1])
                return
        try:
            scale = round(pdf_scale)
        except:
            logger.error('pdf_scale can either be "mhh" (dynamical) or a number. Setting it to 250.')
            scale = 250

        luminosities_file = f'{self.module_dir}/PDFs/parton_luminosities-{pdf_set}-Q{scale}.h5'
        luminosities = DataFrame()
        self.ggHH['pdf_scale'] = scale
        if safe_isfile(luminosities_file):
            logger.info(f'read luminosities from file {luminosities_file}')
            lock = CreateLock(luminosities_file)
            luminosities = read_hdf(luminosities_file)
            lock.release()
        elif lhapdf:
            logger.info(f'Integrating luminosities for {pdf_set} at Q={scale}')
            lock = CreateLock(luminosities_file)
            luminosities = integrate_luminosities(pdf_set, scale, luminosities_file)
            lock.release()
        else:
            logger.error(f'Precomputed luminosities for {pdf_set} at scale {scale} are not available and pyLHAPDF is not installed')
            print('please either use one of the pre-computed luminosities:', listdir(f'{self.module_dir}/PDFs/'), 'or install pyLHAPDF')
            return
        for label,p in partons.items():
            self.lumi[label] = interp1d(luminosities.index, luminosities[(pdf_set, scale, label, 'value')].values, kind=1)
            self.lumi[label] = self.lumi[label]
        self.lumi['q-qbar'] = lambda x : sum(self.lumi[f'{q}-{q}bar'](x) for q in ['u','d','c','s','b'])
        self.lumi['g-q'] = lambda x : sum([self.lumi[label](x) for label in partons if label.count('g') == 1])

    def init_ggHH(self,
                  H1 : str = 'h_SM',
                  H2 : str = 'h_SM',
                  effective_trilinear : Literal['tree-level', 'one-loop', 'one-loop-momentum'] = 'tree-level',
                  propagator_corrections = True,
                  s_channel : list = [],
                  s_channel_only : bool = False,
                  simplify : Union[bool, int] = True,
                  lambdahhh_kwargs : dict = {},
                  pdf_set : str = 'PDF4LHC15_nlo_mc',
                  pdf_scale : Union[float, str] = 250,
                  scalefactor = None,
                  loop_trilinears = [],
                  **kwargs) -> None:
        """
        Initializes the ggHH process for calculating cross-sections.

        Parameters:
        - H1 (str): Name of the first Higgs boson. Default is 'h_SM'.
        - H2 (str): Name of the second Higgs boson. Default is 'h_SM'.
        - effective_trilinear (Literal['tree-level', 'one-loop', 'one-loop-momentum']): Type of effective trilinear coupling to use. Default is 'tree-level'.
        - propagator_corrections: if `effective_trilinear` is `'one-loop'`, take `propagator_corrections` in the OS scheme into account. Requires `wfrs='OS'`.
        - s_channel (list): List of scalar particles to consider in the s-channel. Default is to include all scalars (except Goldstone bosons).
        - s_channel_only (bool): Flag indicating whether to consider only s-channel contributions. Default is False.
        - simplify (bool/int): Flag indicating whether to simplify the expressions. If set to true, only a basic simplification is performed. For more extensive simplification is applied, if `simplify > 1`. Default is True.
        - lambdahhh_kwargs (dict): Additional keyword arguments passed to the `lambdahhh` function. Default is an empty dictionary.
        - pdf_set (str): Name of the PDF set to use. Default is 'PDF4LHC15_nlo_mc'.
        - pdf_scale (float): Scale for the PDF set. Default is 250. Alternatively, the string 'mhh' can be used to indicate that the dynamical scale should be used.
        - scalefactor: Scale factor for the cross-section. Default is None.
        - loop_trilinears: if `effective_trilinear` is set to one-loop, chose for which couplings to include loop-corrections. Empty (default) means that loop-corrections to all trilinear scalar couplings are considered. Example: `loop_trilinears = [['h1','h1','h2'],['h1','h1','h1']]`.

        Returns:
        None
        """
        allowed = ['tree-level', 'one-loop', 'one-loop-momentum']
        if effective_trilinear not in allowed:
            logger.error(f'effective_trilinear "{effective_trilinear}" not supported (choose either of {allowed})!')
            return
        h1, h2 = self._fieldlist([self.SM_particles.get('Higgs-Boson', 'h_SM') if type(f) is str and f == 'h_SM' else f for f in [H1, H2]])
        if not hasattr(self, 'lumi'):
            self.load_PDF(pdf_scale = pdf_scale, pdf_set = pdf_set)

        ggHH = {'H1': h1, 'H2': h2,
                'effective_trilinear': effective_trilinear,
                'propagator_corrections': propagator_corrections,
                's_channel': self._fieldlist(s_channel), 's_channel_only': s_channel_only,
                'lambdahhh_kwargs': lambdahhh_kwargs,
                'pdf_scale': pdf_scale, 'pdf_set': pdf_set,
                'simplify': simplify,
                'scheme': None if effective_trilinear == 'tree-level' else self.scheme_name,
                'loop_trilinears': [sorted([s.name for s in self._fieldlist(coup)]) for coup in loop_trilinears]} # compare inputs with settings stored in self.ggHH

        if self.auto_width is False:
            logger.warning('global `auto_width` is set to `False`. Set it to `True` or make sure to manually set the widths of all relevant scalars!')

        s_channel_scalars = [s for s in self.particles['S'] if (not (s.goldstone or s.anti().goldstone)) and s.charge == 0]
        if s_channel:
            s_channel_scalars = [s for s in s_channel_scalars if s in s_channel or s.name in s_channel]
        logger.debug(f'ggHH: scalars considered in s-channel: {s_channel_scalars}')
        if self.evaluation == 'numerical':
            for s in s_channel_scalars:
                if abs(s.width.nvalue) < 1e-10:
                    logger.error(f"s-channel scalar '{s.name}' has numerically vanishing width!")

        if all(k in self.ggHH and ggHH[k] == self.ggHH[k] for k,v in ggHH.items()):
            # everyting is ready, nothing to be done
            self.ggHH['scalefactor'] = scalefactor if scalefactor else 1
            return
        logger.debug(f'preparing for gg->{h1}{h2} using effective trilinear coupling at {effective_trilinear}:')
        self.ggHH = dict(ggHH)
        self.load_PDF(pdf_scale = pdf_scale, pdf_set = pdf_set)
        self.ggHH['scalefactor'] = scalefactor if scalefactor else 1
        arghash = md5(str(ggHH).encode('utf-8')).hexdigest()

        loaded = 0

        lam_ijk_file = path.join(self.cachedir, f'lam_ijk_gg{h1}{h2}_{effective_trilinear}_{arghash}.json')
        dsigmadt_file = path.join(self.cachedir, f'dsigmadt_gg{h1}{h2}_{effective_trilinear}_{arghash}.json')
        dsigmadt_func_file = path.join(self.cachedir, f'dsigmadt_gg{h1}{h2}_{effective_trilinear}_{arghash}.py')
        lock_file = path.join(self.cachedir, f'gg{h1}{h2}_{effective_trilinear}_{arghash}')

        if self.caching > 0:
            if not safe_isfile(lock_file):
                if safe_isfile(lam_ijk_file):
                    with safe_open(lam_ijk_file, 'r') as f:
                        logger.info(f"reading lam_ijk's contributions from cache ({lam_ijk_file})")
                        self.ggHH['lam_ijk'] = {k: v for k,v in json.load(f).items()}
                        loaded += 1

                if safe_isfile(dsigmadt_file):
                    with safe_open(dsigmadt_file, 'rb') as f:
                        logger.info(f"reading dsigmadt function from cache ({lam_ijk_file})")
                        self.ggHH['dsigmadt'] = json.load(f)
                        loaded += 1

                if safe_isfile(dsigmadt_func_file):
                    for obj in ['dsigmadt_mod','dsigmadt_func','sighat_NLO_virt']:
                        if obj in self.ggHH:
                            del self.ggHH[obj]
                    self.ggHH['dsigmadt_mod'] = lazy_import('dsigmadt', dsigmadt_func_file)
                    if self.evaluation == 'numerical':
                        self.ggHH['dsigmadt_mod'].set_renscale(self.parameters['Qren'].nvalue*self.ggHH['scalefactor'])
                    self.ggHH['dsigmadt_func'] = self.ggHH['dsigmadt_mod'].dsigmadt
                    self.ggHH['sighat_NLO_virt'] = self.ggHH['dsigmadt_mod'].sighat_NLO_virt

                    # make sure to clear function cache
                    for f in inspect.getmembers(self.ggHH['dsigmadt_mod']):
                        if hasattr(f[1],'cache_clear'):
                            f[1].cache_clear()

                    loaded += 1

        if loaded == 3:
            # everything is ready, nothing to be done
            return

        lock = CreateLock(lock_file)

        evalSAVE = str(self.evaluation)
        self.set_evaluation_mode('abbreviations')

        if not self.SM_particles['Higgs-Boson'] or not self.SM_particles['Top-Quark']:
            self.find_SM_particles()
        if 'h_SM' in [H1, H2] and not self.SM_particles['Higgs-Boson']:
            logger.error("Cannot calculate ggh_{SM}h_{SM} without you specifying the SM-like Higgs h_{SM}!")
            return

        t = self.SM_particles.get('Top-Quark', None)
        if not t:
            logger.error('Cannot determine Top quark particle! Specify it in the schemes.yml.')
            return

        Mh1 = self.symbols[h1.mass.name]
        Mh2 = self.symbols[h2.mass.name]
        Mh12 = Mh1**2
        Mh22 = Mh2**2

        MT = self.symbols[t.mass.name]
        MT2 = MT**2
        asMZ = self.symbols[self.SM_parameters['alphaQCD'].name]

        Shat = self.sympify('Shat')
        That = self.sympify('That')

        # sum up s-channel resonances for triangle topology
        FTtriangleHcontr = 0
        FTtriangleAcontr = 0
        self.ggHH['lam_ijk'] = {}
        self.ggHH['lam_ijk_prop'] = {}

        diagonal_propagators = []
        for s in s_channel_scalars:
            # construct propagator
            Ms  = self.symbols[s.mass.name]
            Ms2 = Ms**2
            Gc   = self.symbols[s.width.name]
            propc = 1/(Shat - Ms2 + sp.I*Ms*Gc)
            # get top-Yukawa coupling
            Cstt = self.getcoupling15(s, t, t.anti())
            if not Cstt:
                continue

            force_treelevel = False
            if self.ggHH.get('loop_trilinears', []):
                fields = sorted([s.name for s in self._fieldlist([s,h1,h2])])
                if fields not in self.ggHH['loop_trilinears']:
                    force_treelevel = True

            # construct triangle contribution with tree-level lambda
            if effective_trilinear == 'tree-level' or force_treelevel:
                CsHH = self.getcoupling(s, h1, h2)
                if not CsHH or CsHH['c'].value == '0':
                    continue
                CsHH = self.sympify(f"-I*({CsHH['c'].value})")
                FTtriangleHcontr += Cstt['1']*ff.FTriangleH(MT2,Shat)*propc*CsHH
                FTtriangleAcontr += Cstt['5']*ff.FTriangleA(MT2,Shat)*propc*CsHH
                self.ggHH['lam_ijk'][et(f'EffectiveCoup{s}{h1}{h2}')] = CsHH
            # construct triangle contribution with loop-level lambda
            else:
                mom1,mom2,moms = 0, 0, 0
                if effective_trilinear == 'one-loop-momentum':
                    mom1,mom2,moms = f'{Mh12}', f'{Mh22}', 'Shat'

                lam = self.lambdahhh(fields=[s,h1,h2], momenta=[moms,mom1,mom2], **lambdahhh_kwargs)['total']
                CsHH = f'-1*({lam})'

                if simplify:
                    CsHH = self.sympify(CsHH)
                    CsHH = str(CsHH)
                CsHHprop = '0' # propagator corrections to sHH coupling
                # CsHH = '0' # uncomment and set s_channel_only=True to only get propagator shifts

                # propagator correction to "s -> si"-transition
                CsHHpropabbr = 0
                if propagator_corrections:
                    CsHHprop = '0' # propagator corrections to sHH coupling
                    for x in s_channel_scalars:
                        logger.debug(f'Computing propagator corrections: {x}->{s}')
                        CxHH = self.getcoupling(x,h1,h2)
                        if not CxHH or CxHH['c'].value == '0':
                            continue
                        CxHH = f"-I*({CxHH['c'].name})"
                        if x == s:
                            if x in diagonal_propagators:
                                continue
                            # diagonal selfenergvy
                            diagonal_propagators.append(x)
                            Sigma = self.Sigma(s,momentum="Shat")
                            dM = self.Sigma(s,momentum=f'{Ms2}')
                            dZ = self.Sigmaprime(s,momentum=f'{Ms2}')
                            Sigmahat = f'{Sigma} - ({dM}) - ({dZ})*(Shat - {Ms2})'
                            prop_corr = f'+ ({CxHH})*Re({Sigmahat})*({propc})'
                            if simplify:
                                prop_corr = str(self.sympify(prop_corr))
                            CsHH += '+' + prop_corr
                            continue
                        # off-diagonal selfenergy
                        Mx       = self.symbols[x.mass.name]
                        Mx2      = Mx**2
                        Gx       = self.symbols[x.width.name]
                        propx    = 1/(Shat - Mx2 + sp.I*Mx*Gx)
                        Sigxs_x  = self.Sigma(x,s,momentum=f"{Mx2}")
                        Sigxs_s  = self.Sigma(x,s,momentum=f"{Ms2}")
                        Sigxs_p2 = self.Sigma(x,s,momentum='Shat')

                        # RenSigxs = f'(Shat-{Mx2})*({Sigxs_x})/({Ms2}-{Mx2}) + (Shat-{Ms2})*({Sigxs_s})/({Mx2}-{Ms2}) + {Sigxs_p2}'
                        RenSigxs = f'(Shat-{Ms2})*({Sigxs_x})/({Ms2}-{Mx2}) + (Shat-{Mx2})*({Sigxs_s})/({Mx2}-{Ms2}) + {Sigxs_p2}'
                        prop_corr = f'+ ({CxHH})*Re({RenSigxs})*({propx})'
                        if simplify:
                            prop_corr = str(self.sympify(prop_corr))
                        CsHHprop += '+' + prop_corr

                CsHHabbr = self.sympify(et(f'EffectiveCoup{s}{h1}{h2}'))
                self.ggHH['lam_ijk'][str(CsHHabbr)] = CsHH
                if propagator_corrections:
                    CsHHpropabbr = self.sympify(et(f'PropCorr{s}{h1}{h2}'))
                    self.ggHH['lam_ijk_prop'][str(CsHHpropabbr)] = CsHHprop
                else:
                    CsHHpropabbr = 0
                FTtriangleHcontr += Cstt['1']*ff.FTriangleH(MT2,Shat)*propc*(CsHHabbr + CsHHpropabbr)
                FTtriangleAcontr += Cstt['5']*ff.FTriangleA(MT2,Shat)*propc*(CsHHabbr + CsHHpropabbr)

        # couplings for top box diagram
        nobox = False
        Ch1tt = self.getcoupling15(h1, t, t.anti())
        Ch2tt = self.getcoupling15(h2, t, t.anti())
        if not Ch1tt:
            nobox = True
            Ch1tt1, Ch1tt5 = 0, 0
        else:
            Ch1tt1, Ch1tt5 = Ch1tt['1'], Ch1tt['5']
        if not Ch2tt:
            nobox = True
            Ch2tt1, Ch2tt5 = 0, 0
        else:
            Ch2tt1, Ch2tt5 = Ch2tt['1'], Ch2tt['5']

        # symmetry factor to account for situation
        # in which final state scalars are identical
        if self.ggHH['H1'] == self.ggHH['H2']:
            sym_fac = sp.Rational(1,2)
        else:
            sym_fac = 1

        # obtain equation for dsigma_ggHH/dt
        FTtriangleH = sp.symbols('FTtriangleH')
        FTtriangleA = sp.symbols('FTtriangleA')
        if s_channel_only or nobox:
            self.ggHH['dsigmadt'] = sym_fac/(256*sp.pi)*(asMZ/(4*sp.pi))**2*(
                sp.Abs(FTtriangleH)**2 + sp.Abs(FTtriangleA)**2
                )
        else:
            self.ggHH['dsigmadt'] = sym_fac/(256*sp.pi)*(asMZ/(4*sp.pi))**2*(
                sp.Abs(FTtriangleH
                    + Ch1tt1*Ch2tt1*ff.FBoxHH(MT2, Mh12, Mh22, Shat, That)
                    + Ch1tt5*Ch2tt5*ff.FBoxAA(MT2, Mh12, Mh22, Shat, That)
                       )**2
                + sp.Abs(FTtriangleA + (Ch1tt1*Ch2tt5 + Ch1tt5*Ch2tt1)*ff.FBoxHA(MT2, Mh12, Mh22, Shat, That))**2
                + sp.Abs(
                    + Ch1tt1*Ch2tt1*ff.GBoxHH(MT2, Mh12, Mh22, Shat, That)
                    + Ch1tt5*Ch2tt5*ff.GBoxAA(MT2, Mh12, Mh22, Shat, That))**2
                + sp.Abs((Ch1tt1*Ch2tt5 + Ch1tt5*Ch2tt1)*ff.GBoxHA(MT2, Mh12, Mh22, Shat, That))**2
                )
        # resstr used below to build evaluation function.
        # FTtriangleH/A kept are kept as symbols.
        # Separate cacheable functions are constructed for them.
        resstr = str(self.ggHH['dsigmadt'])
        # substitute FTtriangleH/A and lambdas with explicit expressions for analytic results
        self.ggHH['dsigmadt'] = self.ggHH['dsigmadt'].subs(FTtriangleH, FTtriangleHcontr)
        self.ggHH['dsigmadt'] = self.ggHH['dsigmadt'].subs(FTtriangleA, FTtriangleAcontr)
        # if effective_trilinear != 'tree-level':
        #     for lam, lam_expr in self.ggHH['lam_ijk'].items():
        #         self.ggHH['dsigmadt'] = self.ggHH['dsigmadt'].subs(lam, lam_expr)
        if simplify > 1:
            self.ggHH['dsigmadt'] = self.sympify(self.ggHH['dsigmadt'], simplify=True)
        logger.debug(f"Writing dsigmadt and lam_ijk's to cache ({dsigmadt_file} and {lam_ijk_file}).")
        with open(lam_ijk_file, 'w') as f:
            json.dump({k: str(v) for k, v in self.ggHH['lam_ijk'].items()}, f)
        with open(dsigmadt_file, 'w') as f:
            json.dump(str(self.ggHH['dsigmadt']), f)

        # build arguments for cached functions
        args = '**parameters'

        # helper function for introducing abbreviations
        MZ = self.SM_particles['Z-Boson'].mass.name
        asMZ = self.SM_parameters['alphaQCD'].name

        def introduce_abbr(resstr_in):
            resstr = resstr_in.replace(asMZ, 'asQ')
            resstr = resstr.replace(f'{MT}**2', f'{MT}sq')
            resstr = resstr.replace(f'{Mh12}', f'{Mh1}sq')
            resstr = resstr.replace(f'{Mh1}**4', f'{Mh1}sq2')
            resstr = resstr.replace(f'{Mh22}', f'{Mh2}sq')
            resstr = resstr.replace(f'{Mh2}**4', f'{Mh2}sq2')
            if 'Shat' in resstr_in:
                resstr = resstr.replace('Shat**2', 'Shatsq')
                resstr = resstr.replace('Shat**4', 'Shatsq2')
            if 'That' in resstr_in:
                resstr = resstr.replace('That**2', 'Thatsq')
                resstr = resstr.replace('That**4', 'Thatsq2')
            resstr = resstr.replace('FTtriangleH', f'FTtriangleH(Shat,scale,{args})')
            resstr = resstr.replace('FTtriangleA', f'FTtriangleA(Shat,scale,{args})')
            if effective_trilinear == 'one-loop-momentum': # promote lambdas to functions if @ loop-level with momentum
                for lam in self.ggHH['lam_ijk'].keys():
                    resstr = resstr.replace(lam, f'{lam}(Shat, scale, {args})')
            elif effective_trilinear == 'one-loop':
                for lam in self.ggHH['lam_ijk'].keys():
                    resstr = resstr.replace(lam, f'{lam}(0, scale, {args})') # setting shat=0 allows for caching if no p2 dependence is included in effective trilinear
            if effective_trilinear != 'tree-level' and propagator_corrections: # promote propagator corrections to functions
                for lam in self.ggHH['lam_ijk_prop'].keys():
                    resstr = resstr.replace(lam, f'{lam}(Shat,scale,{args})')
            return resstr

        def def_abbr(fstr):
            funcstr = ''
            for p in self.parameters.values():
                if p.name in fstr:
                    funcstr  += f"    {p.name} = parameters['{p.name}']\n"
            for c in self.couplings.values():
                if c.name in fstr:
                    funcstr  += f"    {c.name} = parameters['{c.name}']\n"
            if 'asQ' in fstr or asMZ in fstr:
                if MZ not in fstr:
                    funcstr  += f"    {MZ} = parameters['{MZ}']\n"
                funcstr  += f'    asQ = RunAlfas({MZ}, scale, {asMZ})\n'
            masses = [str(Mh1), str(Mh2)] if Mh2 != Mh1 else [str(Mh1)]
            masses = ['Shat', 'That', f'{MT}'] + masses
            for m in masses:
                if m not in fstr:
                    continue
                funcstr += f'    {m}sq = {m}**2\n'
                funcstr += f'    {m}sq2 = {m}sq**2\n'
            return funcstr

        # build function to evaluate dsigmadt
        funcstr = (
                   f"# options: {ggHH}\n"
                   "from anyBSM.loopfunctions import lnbar, A0, B0, B00, dB0, dB00, B1, dB1, C0, C1, C2, D0, eps, set_renscale\n"
                   "from anyBSM.physic_utils import RunAlfas, tplus, tminus\n"
                   "import cmath\n"
                   "import functools\n"
                   "from cmath import pi, sin, cos, tan, atan, asin, acos, sqrt, log\n"
                   "import scipy.integrate as integrate\n"
                   "import anyBSM.formfactors as formfactors\n"
                   "from anyBSM.ufo.function_library import complexconjugate, conjugate, Abs, I, Re, Im\n\n"
                   "ff = formfactors.diHiggs(evaluation='numerical')\n\n")
        funcstr += f'def dsigmadt(Shat,That,scale,{args}):\n'
        funcstr += '    set_renscale(scale)\n'
        funcstr += def_abbr(resstr)
        resstr = introduce_abbr(resstr)
        funcstr += f'    return {resstr}'
        # build function to evaluate FTtriangleH
        funcstr += f'\n\ndef FTtriangleH(Shat,scale,{args}):\n'
        resstr = str(FTtriangleHcontr)
        funcstr += def_abbr(resstr)
        resstr = introduce_abbr(resstr)
        funcstr += f'    return {resstr}'
        # build function to evaluate FTtriangleA
        funcstr += f'\n\ndef FTtriangleA(Shat,scale,{args}):\n'
        resstr = str(FTtriangleAcontr)
        funcstr += def_abbr(resstr)
        resstr = introduce_abbr(resstr)
        funcstr += f'    return {resstr}'
        # build functions for lambdas
        if effective_trilinear != 'tree-level':
            for lam, lam_expr in self.ggHH['lam_ijk'].items():
                funcstr += '\n\n@functools.lru_cache(maxsize=1000)\n'
                funcstr += f'def {lam}(Shat,scale,{args}):\n'
                resstr = str(lam_expr)
                funcstr += def_abbr(resstr)
                resstr = introduce_abbr(resstr)
                funcstr += f'    return {resstr}'
            if propagator_corrections:
                for lam, lam_expr in self.ggHH['lam_ijk_prop'].items():
                    funcstr += f'\n\ndef {lam}(Shat,scale,{args}):\n'
                    resstr = str(lam_expr)
                    funcstr += def_abbr(resstr)
                    resstr = introduce_abbr(resstr)
                    funcstr += f'    return {resstr}'
        # coefficient for virtuals
        # this is only for HH finalstates, not for AH, AA!
        # TODO: need routine to determine CP-properties
        c1, c2, c3, NF = 11/2., 4/9., -4/9., 5
        cbox = str(Ch1tt1*Ch2tt1)
        cbox_abbrs = def_abbr(cbox)

        funcstr += f"""\n
@functools.lru_cache(maxsize=None)
def Coeff(s, quad_kwargs={{'limit':200,'epsabs':0,'epsrel':1e-2}},**parameters):
    c2 = {c2}
    c3 = {c3}
{cbox_abbrs}
    Mh12 = parameters['{Mh1}']**2
    Mh22 = parameters['{Mh2}']**2
    MT2 = parameters['{MT}']**2
    tp = tplus(s,Mh12,Mh22).real
    tm = tminus(s,Mh12,Mh22).real
    Cbox = {cbox}

    def integrand(t):
        Fterm = Cbox*(FTtriangleH(s,**parameters) + Cbox*ff.FBoxHH(MT2, Mh12, Mh22, s, t))
        Gterm = Cbox**2*ff.GBoxHH(MT2, Mh12, Mh22, s, t)
        u = Mh12 + Mh22 - s - t
        pT2 = (t-Mh12)*(u-Mh12)/s - Mh12
        return (c2*Fterm + c3*pT2/(2*t*u)*(s-Mh12-Mh22)*Gterm).real
    # TODO: maybe check accuracy here? --> this integration-error is not taken into account in the final estimate!
    return integrate.quad(integrand, tm, tp,**quad_kwargs)[0]

@functools.lru_cache(maxsize=None)
def sighat_NLO_virt(Shat,sighat_LO,scalefactor = 1, quad_kwargs={{'limit':200,'epsabs':0,'epsrel':1e-2}},**params):
    term1 = pi**2 + {c1} + (33-2*{NF})/6.*log(scalefactor**2).real

    asQ = RunAlfas(params['MZ'], scalefactor*sqrt(Shat).real, params['aS'])
    fac    = asQ**2*{sym_fac}/(256.*pi*(4*pi)**2)
    return asQ/pi*(
            term1*sighat_LO(Shat) + fac*Coeff(Shat,**params)
            )
"""
        # save to file
        with open(dsigmadt_func_file, 'w') as f:
            f.write(funcstr)

        for obj in ['dsigmadt_mod','dsigmadt_func','sighat_NLO_virt']:
            if obj in self.ggHH:
                del self.ggHH[obj]
        self.ggHH['dsigmadt_mod'] = lazy_import('dsigmadt', dsigmadt_func_file)

        self.ggHH['dsigmadt_mod'] = lazy_import('dsigmadt', dsigmadt_func_file)
        self.ggHH['dsigmadt_func'] = self.ggHH['dsigmadt_mod'].dsigmadt
        self.ggHH['sighat_NLO_virt'] = self.ggHH['dsigmadt_mod'].sighat_NLO_virt

        self.set_evaluation_mode(evalSAVE)

        lock.release()

    def sigmahat_ggHH(
            self,
            Shat : float,
            pTmin : float = 20.,
            GeV_to_fb : float = 0.38937966e12,
            parameters : Union[dict, str] = {},
            auto_width : bool = True,
            hadronic : bool = False,
            integrator : Literal['vegas', 'nquad'] = 'vegas',
            vegas_options : dict = {'nitn': 5, 'neval': 150, 'summary': False, 'discard': True},
            nquad_options : dict = {'opts': {'limit': 200, 'epsabs': 1e-5, 'epsrel': 1e-5}},
            QCD : list = [],
            Kfactor : float = 2.,
            **kwargs
    ) -> tuple:
        """
        Calculates the differential cross section for the process gg -> HH.

        Parameters:
        - Shat (float): the partonic center-of-mass energy squared.
        - pTmin (float, optional): the minimum transverse momentum.
        - GeV_to_fb (float, optional): conversion factor from GeV to femtobarns.
        - parameters (dict, optional): dictionary of model parameters to set. Defaults to {}.
        - auto_width (bool, optional): If `parameters` are set, the widths of all scalars are re-computed at leading-order.
        - hadronic (bool/float, hadronic): whether to compute hadronic rather than partonic xs. For hadronic xs set it to the center-of-mass energy (e.g. hadronic=14000 for 14TeV).
        - integrator (str, optional): integrator to use (vegas or nquad).
        - vegas_options (dict, optional): options passed to vegas.Inegrator (if `integrator='vegas'`. `discard=True` performs two runs, discarding the first `nitn` iterations to get a more-precise mean. `summary=True` prints summary.
        - nquad_options (dict, optional): default options passed to scipy.nquad (if `integrator='nquad'`).
        - QCD (list, optional): QCD contributions to include in the calculation. Currently not supported.
        - Kfactor (float): K-factor to apply to the result.  If `QCD` is set, this implies Kfactor=1.
        - kwargs: additional keyword arguments passed to `init_ggHH`.

        Returns:
        - result: two-tuple with resulting xs and integration error
        """
        if not self.evaluation == 'numerical':
            logger.error('set evaluation mode to numerical first')
            return (-1,0)
        if parameters:
            self.setparameters(parameters, auto_width = auto_width)
        if not self.parameters['SignSinThetaW'].value:
            self.getSignSinThetaW()

        self.init_ggHH(**kwargs)

        Mh1 = self.ggHH['H1'].nmass
        Mh2 = self.ggHH['H2'].nmass
        Shat = float(Shat)
        beta2 = (1 - (Mh1 + Mh2) ** 2 / Shat) * (
            1 - (Mh1 - Mh2) ** 2 / Shat
        ) - 4 * pTmin**2 / Shat
        if beta2 < 0:
            return [0,0]
        beta = np.sqrt(beta2)
        Tmin = (Mh2**2 - Mh1**2)**2/(4*Shat) - .25*Shat*(1 + beta)**2
        Tmax = (Mh2**2 - Mh1**2)**2/(4*Shat) - .25*Shat*(1 - beta)**2

        dsigmadt_func = self.ggHH['dsigmadt_func']
        mhh = sqrt(Shat).real

        int_options = {'nquad': nquad_options, 'vegas': vegas_options}.get(integrator,{})
        if hadronic and self.ggHH['pdf_scale'] == 'mhh':
            tau = Shat/hadronic**2
            """ dynamical pdf scale """

            def integrand(That,x):
                return GeV_to_fb*luminosity(self.pdf, tau, x, mhh*self.ggHH['scalefactor'],21,21)*dsigmadt_func(Shat, That, mhh*self.ggHH['scalefactor'], **self.all_parameters).real

            return Kfactor*np.array(integrate(integrand, [[Tmin, Tmax],[tau, 1]], integrator=integrator, **int_options))*2.*mhh/hadronic**2

        def integrand(That):
            return GeV_to_fb*dsigmadt_func(Shat, That, self.ggHH['pdf_scale']*self.ggHH['scalefactor'], **self.all_parameters).real

        had = 1.
        if hadronic:
            """ fixed pdf scale -> use luminosity-grid """
            tau = Shat/hadronic**2
            had = self.lumi['g-g'](tau)*2.*mhh/hadronic**2 if hadronic else 1.

        return Kfactor*had*np.array(integrate(integrand, [[Tmin, Tmax]], integrator=integrator, **int_options))

    def sigmahat_NLO_ggHH(self, Shat, **kwargs):
        logger.warning('QCD corrections not fully supported!')
        QCD = kwargs.get('QCD', ['reals','virtuals'])
        if 'hadronic' not in kwargs:
            kwargs['hadronic'] = 14000
        hadronic = float(kwargs['hadronic'])
        kwargs['hadronic'] = False
        S = hadronic**2
        taumin = 2*(self.ggHH['H1'].nmass**2 + self.ggHH['H2'].nmass**2)/hadronic**2
        if kwargs.get('integrate_interpolation', False):
            sigmahat = self.interpolate_sigmahat_ggHH(S*taumin+1e-4,S-1e-4,**kwargs)
        else:
            def sigmahat(x):
                return self.sigmahat_ggHH(x, **kwargs)[0]

        result = {'LO': sigmahat(Shat)}
        lumis = {'LO':'g-g', 'NLO_virtuals': 'g-g', 'NLO_reals_gg': 'g-g', 'NLO_reals_gq': 'g-q', 'NLO_reals_qq': 'q-qbar'}

        if 'virtuals' in QCD:
            result['NLO_virtuals'] = self.ggHH['sighat_NLO_virt'](Shat,sigmahat,**self.all_parameters)
        if 'reals' in QCD:
            QCD = ['reals_gg','reals_gq','reals_qq']
        if 'reals_gg' in QCD:
            result['NLO_reals_gg'] = sighat_NLO_real_gg(Shat/hadronic**2,taumin,hadronic**2,sigmahat,**self.all_parameters)
        if 'reals_gq' in QCD:
            result['NLO_reals_gq'] = sighat_NLO_real_gq(Shat/hadronic**2,taumin,hadronic**2,sigmahat,**self.all_parameters)
        if 'reals_qq' in QCD:
            result['NLO_reals_qq'] = sighat_NLO_real_qq(Shat/hadronic**2,taumin,hadronic**2,sigmahat,**self.all_parameters)
        for k,v in lumis.items():
            r = result.get(k,0)
            result[k] = r*self.lumi[v](Shat/hadronic**2)*2.*Shat/hadronic**2
        result['NLO'] = sum(r for k,r in result.items() if 'NLO_' in k)
        return result

    def calc_sigma_ggHH(
                self,
                S: float,
                pTmin: float = 20,
                GeV_to_fb = 0.38937966e12,
                integrator : Literal['vegas', 'nquad'] = 'vegas',
                vegas_options : dict = {'nitn': 10, 'neval': 5000, 'summary': False, 'discard': True, 'peak_sampling': False},
                nquad_options : dict = {'opts': {'limit': 200, 'epsabs': 1e-5, 'epsrel': 1e-5}},
                integrate_interpolation: dict = {},
                QCD: float = 2.,
                parameters: dict = {},
                auto_width: bool = True,
                **kwargs) -> tuple:
        """
        Calculates the cross section for the process gg -> HH.

        Args:
            S (float): The center-of-mass energy squared.
            pTmin (float, optional): The minimum transverse momentum. Defaults to 20.
            GeV_to_fb (float, optional): Conversion factor from GeV to femtobarns. Defaults to 0.389379e12.
            integrator (str, optional): integrator to use (vegas or nquad).
            vegas_options (dict, optional): options passed to vegas.Inegrator (if `integrator='vegas'`. `discard=True` performs two runs, discarding the first `nitn` iterations to get a more-precise mean. `summary=True` prints summary.
            nquad_options (dict, optional): default options passed to scipy.nquad (if `integrator='nquad'`).
            integrate_interpolation (dict, optional): Dictionary containing the parameters for interpolation. For instance {'n_low': 200, 'n_high': 200, 'n_resonant': 200} will generate an interpolation using n_low points in the region [Mh1+Mh2, MmaxScalar], n_high points in the region [MmaxScalar, S] and n_resonant (gaussian) points around each resonance. The interpolation is then integrated using a simple 1D integral. Works fastest with `integrator='nquad'`. Default = {} (no interpolation done).
            QCD (float, optional): The QCD correction factor. Defaults to 2.
            parameters (dict, optional): Dictionary of model parameters to set. Defaults to {}.
            auto_width (bool, optional): If `parameters` are set, the widths of all scalars are re-computed at leading-order.
            **kwargs: Additional keyword arguments.

        Returns:
            tuple: A tuple containing the total cross section, the total accuracy, and a dictionary with the cross sections at different orders.

        Raises:
            ValueError: If the evaluation mode is not set to 'numerical'.
        """
        if type(QCD) is list:
            logger.warning('QCD corrections not fully supported!')
            Kfactor = 1.
        else:
            Kfactor = float(QCD)
            QCD = []

        if not self.evaluation == 'numerical':
            logger.error('set evaluation mode to numerical first')
            return -1
        if parameters:
            self.setparameters(parameters,auto_width = auto_width)
        if not self.parameters['SignSinThetaW'].value:
            self.getSignSinThetaW()

        self.init_ggHH(**kwargs)

        if 'pdf_scale' in kwargs and self.ggHH.get('pdf_scale',-1) != kwargs['pdf_scale']:
            self.load_PDF(pdf_scale = kwargs['pdf_scale'])
        if 'scalefactor' in kwargs:
            self.ggHH['scalefactor'] = kwargs['scalefactor']

        self.set_evaluation_mode('numerical')

        Mh12 = self.ggHH['H1'].nmass**2
        Mh22 = self.ggHH['H2'].nmass**2
        S = float(S)
        taumin = 2*(Mh12 + Mh22)/S - 1e-6
        taumin = max(taumin,1e-8)

        def beta(tau):
            beta2 = 1 - 2*(Mh12+Mh22)/(S*tau) - 4*pTmin**2/(S*tau)
            if beta2 < 0:
                return 0
            return np.sqrt(beta2)

        def Tmin(tau):
            return 0.5*(Mh12 + Mh22 - tau*S*(1 + beta(tau)))

        def Tmax(tau):
            return 0.5*(Mh12 + Mh22 - tau*S*(1 - beta(tau)))

        result = {'LO': (0,0), 'NLO_virtuals': (0,0), 'NLO_reals_gg': (0,0), 'NLO_reals_gq': (0,0), 'NLO_reals_qq': (0,0)}

        if vegas_options.get('peak_sampling', False):
            if self.ggHH['s_channel']:
                phys_scalar = self.ggHH['s_channel']
            else:
                phys_scalar = [s for s in self.particles['S'] if (not (s.goldstone or s.anti().goldstone)) and s.charge == 0]
            rescond = lambda m: m > self.ggHH['H1'].mass.nvalue.real + self.ggHH['H2'].mass.nvalue.real # noqa: E731
            res_scalars    = [s for s in phys_scalar if rescond(s.mass.nvalue.real)]
            vegas_options['peak_pos'] = [(scalar.nmass, scalar.width.nvalue, S) for scalar in res_scalars]

        int_options = {'nquad': nquad_options, 'vegas': vegas_options}.get(integrator,{})
        if integrate_interpolation and self.ggHH['pdf_scale'] == 'mhh':
            logger.error('interpolation with dynamical pdf scale not implemented. Falling back to full integration.')
            integrate_interpolation = {}
        if integrate_interpolation:
            sigmahat = self.interpolate_sigmahat_ggHH(
                    S*taumin,S,
                    pTmin=pTmin,
                    GeV_to_fb=GeV_to_fb,
                    n_low = integrate_interpolation.get('n_low', 80),
                    n_high = integrate_interpolation.get('n_high', 30),
                    n_resonant = integrate_interpolation.get('n_resonant', 200),
                    QCD = [],
                    Kfactor = 1,
                    integrator = integrator,
                    vegas_options = vegas_options,
                    nquad_options = nquad_options,
                    **kwargs
                    )
            result['LO'] = integrate(lambda tau: self.lumi['g-g'](tau)*sigmahat(tau*S),[[taumin,1]], integrator=integrator, **int_options)
        else:
            dsigmadt_func = self.ggHH['dsigmadt_func']
            if self.ggHH['pdf_scale'] == 'mhh':
                def integrand(That,tau,x):
                    if Tmin(tau) > Tmax(tau):
                        if That > Tmin(tau) or That < Tmax(tau):
                            return 0.
                        print(tau,Tmin(tau),Tmax(tau))
                    else:
                        if That < Tmin(tau) or That > Tmax(tau):
                            return 0.
                    if x < tau:
                        return 0.
                    mhh = sqrt(S*tau).real
                    return GeV_to_fb*luminosity(self.pdf, tau, x, mhh*self.ggHH['scalefactor'],21,21)*dsigmadt_func(S*tau, That, mhh*self.ggHH['scalefactor'], **self.all_parameters).real
                result['LO'] = integrate(integrand, [[-S, 0],[0., 1.],[0.,1.]], integrator=integrator, **int_options)
            else:
                def integrand(That,tau):
                    if Tmin(tau) > Tmax(tau):
                        if That > Tmin(tau) or That < Tmax(tau):
                            return 0.
                        print(tau,Tmin(tau),Tmax(tau))
                    else:
                        if That < Tmin(tau) or That > Tmax(tau):
                            return 0.
                    return GeV_to_fb*self.lumi['g-g'](tau)*dsigmadt_func(S*tau, That, self.ggHH['pdf_scale']*self.ggHH['scalefactor'], **self.all_parameters).real
                result['LO'] = integrate(integrand, [[0.5*(Mh12+Mh22-S), 0.5*(Mh12+Mh22)],[taumin, 1]], integrator=integrator, **int_options)

            def sigmahat(s):
                return self.sigmahat_ggHH(s,GeV_to_fb=GeV_to_fb,QCD=[])

        if 'virtuals' in QCD:
            result['NLO_virtuals'] = integrate(
                    lambda tau: self.lumi['g-g'](tau)*self.ggHH['sighat_NLO_virt'](tau*S,sigmahat,**self.all_parameters),
                    [[taumin,1-1e-8]])

        if 'reals' in QCD:
            QCD = ['reals_gg','reals_gq','reals_qq']

        if 'reals_gg' in QCD:
            result['NLO_reals_gg'] = integrate(
                    lambda tau: self.lumi['g-g'](tau)*sighat_NLO_real_gg(tau,taumin,S,sigmahat,**self.all_parameters),
                    [[taumin,1-1e-8]])

        if 'reals_gq' in QCD:
            result['NLO_reals_gq'] = integrate(
                    lambda tau: self.lumi['g-q'](tau)*sighat_NLO_real_gq(tau,taumin,S,sigmahat,**self.all_parameters),
                    [[taumin,1-1e-8]])

        if 'reals_qq' in QCD:
            result['NLO_reals_qq'] = integrate(
                    lambda tau: self.lumi['q-qbar'](tau)*sighat_NLO_real_qq(tau,taumin,S,sigmahat,**self.all_parameters),
                    [[taumin,1-1e-8]])
        total = Kfactor*sum([i[0] for i in result.values()])
        total_acc = Kfactor*sqrt(sum([i[1]**2 for i in result.values()])).real

        if result['LO'][0] == 0:
            result['Kfactor'] = 0.
        else:
            result['Kfactor'] = total/result['LO'][0]

        return total, total_acc, result

    def interpolate_sigmahat_ggHH(self, Smin,Smax, n_resonant = 250, n_low = 250, n_high = 100, kwargs_interp1d={}, **kwargs):
        # TODO: cache this interpolation and clear it when setparameters is called?
        """ interpolates sigmahat_ggHH using a grid based that is optimised on the position of resonances.
        Resonances are sampled using a gaussian distributions with a variance of max(2*width_of_particle,10).

         Args:
            * Smax, Smin: interpolation region
            * n_resonant: number of (gaussian sampled) sample points for each resonance
            * n_low: number of (logaritmically sampled) points in the region [sqrt(Smin), max(all_masses)]
            * n_high: number of (logaritmically sampled) points in the region [max(all_masses), sqrt(Smax)]
            * kwargs_interp1d: arguments passed to scipy.interpolate.interp1d
            * kwargs: all additional arguments are passed to sigmahat_ggHH
         Returns:
            scipy interpolation
        """

        maxmass = max([p.mass.nvalue.real for p in self.particles['S']]+[500])
        log10 = np.log10

        lowint = np.logspace(log10(sqrt(Smin)),log10(maxmass), n_low).real
        highint = np.logspace(log10(maxmass), log10(sqrt(Smax)), n_high).real
        logger.debug(f'sampling {n_low} points from {min(lowint)} to {max(lowint)}')
        logger.debug(f'sampling {n_high} points from {min(highint)} to {max(highint)}')

        resonances = []
        massvals = []

        if self.ggHH.get('s_channel',[]):
            s_channel_scalars = self.ggHH['s_channel']
        else:
            s_channel_scalars = [s for s in self.particles['S'] if (not (s.goldstone or s.anti().goldstone)) and s.charge == 0]

        for p in self._fieldlist(s_channel_scalars):
            m = p.mass.nvalue.real
            if m**2 < Smin:
                continue
            try:
                if p.width.nvalue == 0:
                    logger.warning(f'Resonance "{p.name}" has a vanishing width. This may cause numerical instabilities!')
            except:
                pass
            massvals.append(m)
            width = p.width.nvalue*p.mass.nvalue
            if width.real < 10:
                width = 10
            logger.debug(f'sampling resonance {p.name} with {p.mass.name}={p.mass.nvalue} and {n_resonant} points.')
            resonances.append(np.random.normal(loc=m, scale=width, size=n_resonant))

        points = set(sorted(np.concatenate([lowint] + [highint] + resonances)))

        grid = np.array([[s, self.sigmahat_ggHH(s**2, **kwargs)[0]] for s in tqdm(points, desc='sampling points for interpolation', leave=False, disable = not self.progress)]).transpose()

        self.ggHH['sigmahat_ggHH_interpolation'] = interp1d(grid[0]**2, grid[1], **kwargs_interp1d)
        return self.ggHH['sigmahat_ggHH_interpolation']

    def sigma_ggHH(
            self,
            S: float,
            calc_individual_res_sigma: bool = False,
            **kwargs) -> dict:
        """
        Compares the resonant and non-resonant pair-production cross section for gg->HH.

        The resonant cross-section is calculated by including only contributions with
        a scalar in the s-channel whose mass is larger than the sum of the final-state Higgs masses.
        The non-resonant cross-section excludes these contributions and takes into account the box
        diagrams as well as non-resonant scalars in the s-channel.

        Args:
            S (float): Center-of-mass energy in GeV.
            calc_individual_res_sigma (bool, optional): If True, the individual resonant cross-sections are calculated. Defaults to False.
            **kwargs: Keyword arguments passed to `calc_sigma_ggHH`.

        Returns:
            dict: Cross-sections for gg->HH in pb for the resonant, non-resonant, and all contributions.
        """

        if not hasattr(self, 'res_vs_nonres_warning'):
            logger.warning("""
The resonant and non-resonant cross-sections should not be compared
directly to the respective experimental searches. The experimental
searches typically define a signal region optimized for a large
fraction of either resonant or non-resonant pair production which
does not correspond to the definition of resonant and non-resonant
cross-sections used here. Therefore, if wanted, the user has to find
a self-defined criterion to decide which part of the calculated
cross-section should be confronted with resonant or
with non-resonant pair-production limits.
            """)
            self.res_vs_nonres_warning = False

        self.init_ggHH(**kwargs)

        if self.ggHH['s_channel']:
            phys_scalar = self.ggHH['s_channel']
        else:
            phys_scalar = [s for s in self.particles['S'] if (not (s.goldstone or s.anti().goldstone)) and s.charge == 0]

        rescond = lambda m: m > self.ggHH['H1'].mass.nvalue.real + self.ggHH['H2'].mass.nvalue.real # noqa: E731
        nonres_scalars = [s for s in phys_scalar if not rescond(s.mass.nvalue.real)]
        res_scalars    = [s for s in phys_scalar if rescond(s.mass.nvalue.real)]

        desc = 'computing di-Higgs (%s)'
        total = 2
        if res_scalars:
            total += 1 if not calc_individual_res_sigma else 1 + len(res_scalars)
        pbar = tqdm(
            desc = desc % "all contributions",
            total = total,
            leave = False, disable = not self.progress)

        XS_all = self.calc_sigma_ggHH(S, **kwargs)
        pbar.update()
        Kfactor = XS_all[2]['Kfactor']
        XS_all = XS_all[0:2]
        kwargs['s_channel'] = nonres_scalars
        pbar.set_description(desc % "HH_non-resonant: only non-resonant scalars + box diagrams")
        XS_nonres = self.calc_sigma_ggHH(S, **kwargs)[0:2]
        pbar.update()
        if res_scalars == []:
            XS_res = (0., 0.)
        else:
            kwargs['s_channel'] = res_scalars
            kwargs['s_channel_only'] = True
            pbar.set_description(desc % "HH_resonant: only resonant scalars and no box diagrams")
            XS_res = self.calc_sigma_ggHH(S, **kwargs)[0:2]
            pbar.update()

        if calc_individual_res_sigma:
            XS_ind = {}
            for s in res_scalars:
                kwargs['s_channel'] = [s]
                kwargs['s_channel_only'] = True
                pbar.set_description(desc % f"individual_resonant: only resonant {s.name}-contribution")
                XS_ind[s.name] = self.calc_sigma_ggHH(S, **kwargs)[0:2]
                pbar.update()
            pbar.close()
            return {'HH_all': XS_all, 'HH_non-resonant': XS_nonres, 'HH_resonant': XS_res, 'individual_resonant': XS_ind, 'Kfactor': Kfactor}
        else:
            pbar.close()
            return {'HH_all': XS_all, 'HH_non-resonant': XS_nonres, 'HH_resonant': XS_res, 'resonant_scalars': res_scalars, 'Kfactor': Kfactor}
