from __future__ import division
import logging
from tqdm.auto import tqdm
from os import path
import json
from collections import ChainMap
from hashlib import md5
from typing import Union
import cmath # noqa: F401
from cmath import pi, sin, cos, tan, atan, sqrt, log # noqa: F401
from anyBSM.loopfunctions import lnbar, A0, B0, B00, dB0, dB00, B1, dB1, C0, C1, C2, eps, set_renscale # noqa: F401
from anyBSM.ufo.function_library import complexconjugate, conjugate, Abs, I, Re, Im # noqa: F401
from anyBSM.anyProcess import anyProcess
from anyBSM.utils import import_sympy
from anyBSM.locks import CreateLock, safe_isfile

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

__doc__ = """
<div class="mermaid">
flowchart RL
    subgraph classes
    anyModel --> anyProcess --> anyH3;
    end
    subgraph methods
    anyH3 --> lambdahhhCT --> lambdahhh
    end
</div>
"""

class anyH3(anyProcess):
    r""" Calculation of the trilinear Higgs self coupling in OS/$\MS$/user-defined renormalization schemes.

    This class inherits from `anyBSM.anyProcess` which itself inherits from `anyBSM.anyModel`.
    For more information on how to initialize this class, see the documentation of the mother-class `anyBSM.anyModel.anyModel.__init__`  or consult the online documentation.
    The class `anyBSM.anyH3` implements two methods:
      * `anyBSM.anyH3.anyH3.lambdahhh()`: actual computation of the full one-loop corrected trilinear self-coupling (including the renormalization-specific contributions)
      * `anyBSM.anyH3.anyH3.lambdahhhCT()`: automatic caclulation of all renormalization-specific contributions

    It is assumed that a proper renormalization scheme has already been loaded and all SM-like particles/parameters are correctly identified.
    To ensure this `anyBSM.anyModel.anyModel.load_renormalization_scheme()` should be issued beforehand (see example below).

    Example
    ```python
    from anyBSM import anyH3
    SM = anyH3('SM')
    SM.load_renormalization_scheme('OS')
    lam = SM.lambdahhh()
    lamLO = lam[0]
    lamNLO = sum(lam).real
    lamNLO/lamLO
    ```
    """
    def lambdahhh(
        self,
        fields : list = ['h_SM','h_SM','h_SM'],
        momenta: list = [0,0,0],
        only_topologies: list = [],
        simplify: bool = False,
        draw: Union[bool, int] = False,
        tadpoles: bool = None,
        wfrs: bool = None,
        exclude_particles: list = [],
        exclude_particles_pdg: list = [],
        exclude_CTs: list = [],
        parameters = {}
    ) -> dict:
        """ Wrapper around anyBSM.process and anyH3.lambdahhhCT:
            1. Ensures that the SM-like Higgs boson $h$ is correctly
            set/found in the model for the specified parameter set.
            2. Calculates all genuine tree-level and one-loop diagrams
            to $\\lambda_{hhh}$ as well as external-leg-,
            tadpole- and counter-term-contributions.
            3. Returns tuple of individual contributions

            Args:
                fields: specify external scalars
                momenta: specify external momenta
                    only_topologies: consider only diagrams from specific
                    topologies (see `anyBSM.topologies`)
                simplify: simplify results using sympy
                draw: whether to draw Feynman diagrams using
                    [tikz-feynman](https://arxiv.org/abs/1601.05437). If set to
                    a number !=0 and in evaluation mode 'numerical',
                    this number is used as a cut-off to draw only diagram which
                    contribute with an absolute value larger than the cut-off.
                tadpoles: turn on (`True`) or off (`False`) all tadpole-insertion contributions i.e. all non-PI tadpole diagrams (warning: this is
                    not physically correct and needs to be compensated by using a customCT in the schemes.yml).
                    Can also be controlled from within the schemes.yml using the `tadpole: True/False` statement.
                    Default: `None` (use schemes.yml which defaults to `True`).
                wfrs: turn on (`True`) or off (`False`) all external diagrams with external-leg corrections.
                    Similar to the tadpoles, this can be set in the schemes.yml as well.
                    Default: `None` (uses schemes.yml which defaults to `True`)
                exclude_particles: list of fields (listed by names) to exclude from diagrams -- passed on to anyBSM.process and anyH3.lambdahhhCT.
                exclude_particles_pdg: list of fields (listed by PDG numbers) to exclude from diagrams -- passed on to anyBSM.process and anyH3.lambdahhhCT.
                parameters: optionally run `anyBSM.anyModel.setparameters(parameters)` so set input parameters
                exclude_CTs: list of parameter_counterterms (defined in schemes.yml) to ignore in the calculation. This is useful when determining those CTs via OS conditions

            Return:
                dictionary of the individual contributions: total, treelevel, genuine, wfr, tadpoles, massren, vevren, customren
        """

        if len(fields) != 3:
            logger.error('`fields` arguments of lambdahhh should be a list of length 3!')
            return
        if parameters:
            self.setparameters(params=parameters)
        if not self.SM_particles['Higgs-Boson']:
            self.find_SM_particles()
        if 'h_SM' in fields and not self.SM_particles['Higgs-Boson']:
            logger.error("Cannot calculate lambda_hhh without you specifying what h to use!")
            return
        fields = [self.SM_particles.get('Higgs-Boson', 'h_SM') if type(f) is str and f == 'h_SM' else f for f in fields]
        h1, h2, h3 = self._fieldlist(fields)
        if momenta[0] == "auto":
            if self.evaluation == 'numerical':
                momenta = [h1.nmass**2, h2.nmass**2, h3.nmass**2]
            else:
                momenta = ['p12','p22','p32']

        results, drawer = self.process(h1, h2, h3,
                               momenta = momenta,
                               only_topologies = only_topologies,
                               tadpoles = tadpoles,
                               wfrs = wfrs,
                               simplify = simplify,
                               draw = draw,
                               exclude_particles = exclude_particles,
                               exclude_particles_pdg = exclude_particles_pdg,
                               return_drawer = True)

        treelevel = self._eval(f"-({results['ThreePointTree']})")
        genuine = self._eval(f"-({results['ThreePointB']} + {results['ThreePointC']})")
        wfr = self._eval(f"-({results['ThreePointWFRA']} + {results['ThreePointWFRB']})")
        tads = self._eval(f"-({results['ThreePointWFRT']} + {results['ThreePointTA']})")

        CTs = self.lambdahhhCT(
                h1, h2, h3,
                momenta = momenta,
                simplify = simplify,
                draw = False,
                only_topologies = only_topologies,
                tadpoles = tadpoles,
                exclude_particles = exclude_particles,
                exclude_particles_pdg = exclude_particles_pdg,
                exclude_CTs = exclude_CTs)

        vevren = self._eval(CTs['VEV_CT'])
        massren = self._eval(CTs['mass_CTs'])
        customren = self._eval(CTs['customCT'])

        results['ThreePointCT'] = self._eval(f'- {vevren} - {massren} - {customren}')
        if draw and drawer:
            drawer.add_diagram(
                    'ThreePointCT',
                    [],
                    results['ThreePointCT'])
            drawer.write(results)

        total = treelevel + genuine + wfr + tads + vevren + massren + customren
        return {
            'total': total,
            'treelevel': treelevel,
            'genuine': genuine,
            'wfr': wfr,
            'tads': tads,
            'massren': massren,
            'vevren': vevren,
            'customren': customren
            }

    def lambdahhhCT(
            self,
            h1, h2, h3,
            simplify: bool = False,
            momenta : list = [0,0,0],
            only_topologies: list = [],
            draw: Union[bool, int] = False,
            tadpoles: bool = None,
            exclude_particles: list = [],
            exclude_particles_pdg: list = [],
            exclude_CTs: list = []
    ) -> dict:
        """ Calculates the counter-term contribution to the trilinear
        self-coupling at the one-loop order according to the
        specification of the loaded renormalization scheme (see
        anyBSM.load_renormalization_scheme()).\n
        The calculation involves several steps:
            1. Calculate the tree-level value for $\\lambda_{h_1h_2h_3}$ and
            express it in terms of all external parameters (masses, VEVs):
            $\\lambda_{hhh}(m_{h_1}, m_{h_2}, \\dots, v_{SM},
            v_{i},\\dots$).
            2. If `VEV_counterterm: OS` was specified in the
            renormalization scheme, it calculates
            $$\\delta^{(1)}_{v_{SM}}\\lambda_{h_1h_2h_3} = \\frac{\\partial
            \\lambda_{hhh}}{\\partial v_{SM}}\\delta^{(1)}v_{SM}$$
            with
            $$\\delta^{(1)}v_{SM} = v_{SM}\\left(\\frac{\\delta^{(1)}M_W^2}{M_W^2} - \\frac{\\delta^{(1)}M_Z^2}{2M_Z^2} -\\frac{\\delta^{(1)}e}{e} - \\frac{\\delta^{(1)}M_W^2}{2\\sin^2\\theta_w} + \\frac{\\delta^{(1)}M_Z^2}{2\\sin^2\\theta_w}\\right)$$
            where the OS counter-terms for the W- and Z-boson as well as the
            electric charge are extracted from the transverse parts of the
            W-, Z-,  Photon- and Z-Photon-selfenergies. See [2305.03015](https://arxiv.org/abs/2305.03015) for more details.
            3. For all `'OS'`-entries `mass_counterterms: {'hi': 'OS',
            ...}` the contribution
            $$\\delta^{(1)}_{m_{h_i}}\\lambda_{hhh} = -\\frac{\\partial
            \\lambda_{hhh}}{\\partial m_{h_i}}\\frac{1}{2
            m_{h_i}}\\mathrm{Re}\\Sigma^{(1)}_{h_i}(p^2=m_{h_i}^2)$$
            is calculated.
            4. If a `custom_counterterm` with `process: [h1, h2, h3]` has been specified in the renormalization
            scheme, the supplied `condition` is being evaluated. For instance
            a valid value could look like:

                 <!-- language: yml -->

                    [...]
                    wfrs: false
                    parameter_counterterms:
                        - parameter: betaH
                          counterterm: dbetaH
                          condition: (Re(Sigma('Hm1','Hm2',momentum='0')) + Re(Sigma('Hm1','Hm2',momentum='MHm2**2')))/(2*MHm2**2)
                    custom_counterterms:
                        - process: [h1, h1, h1]
                          condition: |
                            dZhh = Sigmaprime('h', momentum='Mh**2')
                            customCT = 3*Mh**2/vvSM*3/2*dZhh"

             the value of `customCT` **must** be set in order to
            add the contribution to the final result:
            $$\\delta^{(1)}_{\\text{custom}}\\lambda_{h_1h_2h_3} = {\\tt \\text{customCT}} + \\sum_{x}^{\\tt \\text{parameter_counterterms}} \\frac{\\partial \\lambda_{h_1 h_2 h_3}}{\\partial x} \\delta x$$
            The `customCT` provides access to the full `self` object of
            the model with all its methods such as `anyBSM.Derivative`,
            `anyBSM.Sigma`, `anyBSM.Sigmaprime` or `anyBSM.process. This is
            particularly useful for more complex renormalization schemes. Note that specific choices of momenta, or restrictions on topologies, particles, must be included in the customCT definition. Contributions from defined `parameter_counterters` via the vertex counterterms of the considered amplitude are taken into account automatically and must not be specified in the `custom_counterterms` (their definitions can, however, be reused therein).

        Args:
            simplify:  simplify results using sympy
            only_topologies: consider only diagrams from specific
                topologies (see `anyBSM.topologies`)
            draw: whether to draw Feynman diagrams using
                [tikz-feynman](https://arxiv.org/abs/1601.05437). If set to
                a number !=0 and in evaluation mode 'numerical',
                this number is used as a cut-off to draw only diagram which
                contribute with an absolute value larger than the cut-off.
            tadpoles: turn off all tadpole-insertion contributions (warning: this is
                not physically correct unless a customCT with the correct tadpole treatment is added!)
            exclude_particles: list of fields (listed by names) to exclude from diagrams -- passed on to anyBSM.Sigma and anyBSM.Sigmaprime via sigmaargs.
            exclude_particles_pdg: list of fields (listed by PDG numbers) to exclude from diagrams -- passed on to anyBSM.Sigma and anyBSM.Sigmaprime via sigmaargs.
            exclude_CTs: list of parameter_counterterms (defined in schemes.yml) to ignore in the calculation. This is useful when determining those CTs via OS conditions

        Returns:
            A dictionary `{`$\\text{'VEV_CT'}:
            \\delta^{(1)}_{v_{SM}}\\lambda_{hhh},\\, \\text{'mass_CTs'}:
            \\sum_{i} \\delta^{(1)}_{m_{h_i}}\\lambda_{hhh},\\,
            \\text{'customCT'}: \\delta^{(1)}_{\\text{custom}}\\lambda_{hhh}$ `}`
        """

        if not self.scheme:
            logger.warning("no renormalization scheme defined. Assuming all input parameters are MSbar values.")
            return {'VEV_CT': '', 'mass_CTs': '', 'custom_CT': ''}
        argstring = f'Topologies: {only_topologies}; Particle exclusions: {exclude_particles}, {exclude_particles_pdg}; Tadpoles: {tadpoles}; Excluded CTs: {exclude_CTs}'
        argshash = md5(argstring.encode('utf-8')).hexdigest()
        CTfile = path.join(self.cachedir, f'result_{h1}{h2}{h3}CT_{self.scheme_name}_scheme_{argshash}.json')
        momenta_d = {f'PEXT{i+1}2' : m for i,m in enumerate(momenta)}
        if self.caching > 1 and safe_isfile(CTfile):
            with open(CTfile, 'r') as f:
                logger.info(f'reading counterterm contributions for "{self.scheme_name}"-scheme from cache ({CTfile})')
                result = json.load(f)
            if self.evaluation == 'numerical':
                momenta_d = {k: self._eval(v) for k,v, in momenta_d.items()}
                for k,v in result.items():
                    try:
                        result[k] = self._eval(v, **momenta_d)
                    except Exception as e:
                        raise Exception(f"There was an error when evaluating the '{k}'-counterterm contribution") from e

            elif self.evaluation == 'analytical':
                for r in result:
                    for m,md in momenta_d.items():
                        result[r] = result[r].replace(m, str(md))
                    for name,c in reversed(self.couplings.items()):
                        result[r] = result[r].replace(name, f'({c.value})')
            return result
        firstrun = self.caching > 1 and self.evaluation != 'abbreviation' and not only_topologies and not draw
        if firstrun:
            evalSAVE = self.evaluation
            logger.info('No CT results found in cache. Calculating in abbreviation-mode to fill cache.')
            self.set_evaluation_mode('abbreviations')
            lock = CreateLock(CTfile)

        # options used for calculation of all two-point funtions in the following
        sigmaargs = {
                'momentum': 'auto' if self.OSmomenta else 0,
                'only_topologies': only_topologies,
                'tadpoles': tadpoles,
                'simplify': simplify,
                'draw': draw,
                'exclude_particles': exclude_particles,
                'exclude_particles_pdg': exclude_particles_pdg}

        OSlist = [self.all_particles[k] for k,v in self.scheme['mass_counterterms'].items() if v == 'OS']
        treelevel_needed = OSlist or self.scheme['VEV_counterterm'] == 'OS' or self.scheme['custom_counterterms'] or self.scheme['parameter_counterterms']
        # calculate tree-level analytical (if not already)
        if treelevel_needed and ((h1,h2,h3) not in self.treelevel or self.evaluation != 'analytical'):
            logger.info('calculate counterterm contributions to lambda_hhh')
            evalSAVE2 = str(self.evaluation)
            self.set_evaluation_mode('analytical') # enforce analytic evaluation to be able to perform derivatives

            logger.info('Calculating tree-level value of lambda_hhh and expressing it in terms of all input parameters.')
            self.treelevel[(h1,h2,h3)] = self.process(h1, h2, h3, momenta = [], only_topologies = ['ThreePointTree'], simplify = simplify)['ThreePointTree']
            self.treelevel[(h1,h2,h3)] = self.treelevel[(h1,h2,h3)].replace('cmath.','')

            treemasses = [p.mass.name for p in OSlist]

            self.treelevel[(h1,h2,h3)] = -1*self.SolveDependencies(self.treelevel[(h1,h2,h3)], simplify = simplify, exclude = treemasses+[self.SM_parameters['VEV'].name])
            logger.info(f'lamda_hhh^tree = {self.treelevel[(h1,h2,h3)]}')

            treemasses = [m for m in treemasses if m in str(self.treelevel[(h1,h2,h3)])]
            logger.info(f"found the following scalar masses appearing at the tree-level: {treemasses}")

            self.set_evaluation_mode(evalSAVE2)

        # contributions from mass renormalization
        OSliststr = [f"{p.name} ({p.mass})" for p in OSlist]
        logger.info(f"the masses of the following particles are renormalized OS: {' '.join(OSliststr) if OSlist else 'none'}")
        massren = self._eval('')
        for p in tqdm(OSlist, leave = False, disable = not self.progress, desc = 'calculate mass CT contributions'):
            logger.info(f'taking derivative of tree-level result w.r.t. {p.mass.name}')
            massderivative = self.Derivative(self.treelevel[(h1,h2,h3)], p.mass.name, simplify = simplify, dependencies = False)
            if massderivative == '0' or not massderivative:
                logger.warning(f'derivative of lambda_hhh^tree w.r.t {p.mass.name} vanishes!')
                continue
            selfenergy = self.Sigma(p.name, **sigmaargs)
            massren += self._eval(f"-({massderivative})*Re({selfenergy})/(2*{p.mass})")

        # contributions from vev renormalization
        vevren = self._eval('')
        vevderivative = '0'
        if self.scheme['VEV_counterterm'] == 'OS':
            logger.info('calculate CT contribution due to electroweak VEV')
            logger.info(f'taking derivative of tree-level result w.r.t. {self.SM_parameters["VEV"].name}')
            vevderivative = self.Derivative(self.treelevel[(h1,h2,h3)], self.SM_parameters['VEV'].name, simplify = simplify, dependencies = False)
            if str(vevderivative) == '0':
                logger.warning('derivative of lambda_hhh^tree w.r.t the SM VEV vanishes!')

        if str(vevderivative) != '0':
            # parameters
            MW2 = self.getmass('W-Boson', 2)
            MZ2 = self.getmass('Z-Boson', 2)
            CW = f'sqrt(({MW2})/({MZ2}))'
            SW = f'(sqrt(1-({MW2})/({MZ2})))'
            alphaQEDinverse = f"1/({self.SM_parameters['alphaQEDinverse']})"
            EL = f"(2*sqrt({alphaQEDinverse})*sqrt(pi))"
            VEV = f'2*sqrt(({MW2}*{MZ2}-{MW2}**2)/{MZ2})/{EL}'
            dalpha = self.SM_parameters['Dalpha'].name

            # W/Z selfenergies
            SigmaW = self.Sigma(self.all_particles['W-Boson'], **sigmaargs)
            SigmaZ = self.Sigma(self.all_particles['Z-Boson'], **sigmaargs)

            # Photon(-Z-mixing) selefenergies
            SigmapAnolf = self.Sigmaprime(
                    self.all_particles['Photon'],
                    **ChainMap({'momentum': 0, 'exclude_particles_pdg': list(set(exclude_particles_pdg) | set(self.light_SM_fields))}, sigmaargs))
            SigmaAlf = self.Sigma(
                    self.all_particles['Photon'],
                    **ChainMap({'momentum': MZ2, 'exclude_particles_pdg': list(set(exclude_particles_pdg) | set(self.heavy_fields))}, sigmaargs))
            SigmaAZ = self.Sigma(
                    self.all_particles['Photon'], self.all_particles['Z-Boson'],
                    **ChainMap({'momentum': 0}, sigmaargs))

            # counterterms
            deltaMW = f'Re({SigmaW})/({MW2})'
            deltaMZ = f'Re({SigmaZ})/({MZ2})'
            deltaEL = f'(1/2*(({dalpha}) + Re({SigmapAnolf}) + Re({SigmaAlf})/({MZ2}))\
                    +  SignSinThetaW*({SW}/({CW}*{MZ2}))*Re({SigmaAZ}))'
            vevCT = f'{VEV}*(({deltaMW})/2 - {deltaEL} +\
                    ({CW}**2)/(2*{SW}**2)*(({deltaMZ}) - ({deltaMW})))'

            vevren += self._eval(f'({vevCT})*({vevderivative})')

        if self.scheme['custom_counterterms'] or self.scheme['parameter_counterterms'] or self.scheme.get('custom_CT_hhh', False):
            # define a few convenient functions which can directly be used in the custom_CT_hhh string
            Tadpole = self.Tadpole           # noqa: F841
            Sigmaprime = self.Sigmaprime     # noqa: F841
            Sigma = self.Sigma               # noqa: F841
            Derivative = self.Derivative     # noqa: F841
            process = self.process           # noqa: F841
            lambdahhh = self.lambdahhh       # noqa: F841
            sympify = self.sympify       # noqa: F841
            getcoupling = self.getcoupling   # noqa: F841
            SM_particles = self.SM_particles # noqa: F841
            treelevel = self.treelevel[(h1,h2,h3)]  # noqa: F841
            lambdahhh_tree = treelevel  # noqa: F841 # v1.0 legacy
            self.sympify_parameters()
            self.symbols['Derivative'] = Derivative
            self.symbols['getcoupling'] = getcoupling
            self.symbols['Tadpole'] = lambda *x,**y: sympify(Tadpole(*x,**y))
            self.symbols['Sigma'] = lambda *x,**y: sympify(Sigma(*x,**y))
            self.symbols['Sigmaprime'] = lambda *x,**y: sympify(Sigmaprime(*x,**y))
            self.symbols['process'] = process
            self.symbols['lambdahhh'] = lambdahhh
            sp = import_sympy()

        customren = self._eval('')

        # sympify user defined parameter counterterms
        self.parameter_counterterms = {}
        all_cts = {}
        for pc in self.scheme['parameter_counterterms']:
            if 'parameter' not in pc:
                logger.error(f'No UFO "parameter" defined for counterterm: {pc}')
                continue
            if pc['counterterm'] in exclude_CTs or pc['parameter'] in exclude_CTs:
                logger.debug(f'skipping CT for {pc}')
                continue
            if pc.get('warn', True):
                param = self.parameters.get(pc['parameter'], None)
                if not param:
                    logger.error(f"Parameter {pc['parameter']} not found in the UFO model!")
                if param and param.nature != 'external':
                    logger.warning((f"Can't automatically take into account the counterterm of a parameter ({param.name}) "
                                    "which is not defined as UFO input parameter."))
                    logger.warning(("It's contribution needs to be added manually using a custom counterterm."
                                    "It is possible to reuse it's definition therein."
                                    "If you already do so, you can disable this warning by setting `warn: False` for this counterterm."))
            ctname = pc.get('counterterm', None)
            if not ctname: # automatically assing CT name if no one was defined
                ctname = 'd' + pc['parameter']
            if ctname in self.parameters: # avoid conflicts with other UFO parameters
                logger.warning(f'counterterm name {ctname} conflicts with already defined UFO parameter!')
                ctname = 'deltaCT' + pc['parameter']
            ct = pc.get('condition', None)
            if not ct:
                logger.error(f"No counterterm condition defined for {pc['parameter']} ({ctname})")
                continue
            self.symbols[ctname] = sp.Symbol(ctname)
            self.symbols_values[ctname] = self.SolveDependencies(ct, simplify = simplify)
            all_cts[ctname] = self._eval(str(self.symbols_values[ctname]), **all_cts)
            logger.info(f'caclulating contrubution from custom counterterm {ctname} onto {(h1,h2,h3)}-amplitude')
            ct_contrib = self.Derivative(self.treelevel[(h1,h2,h3)], pc['parameter'], simplify = False)*self.symbols_values[ctname]
            if simplify:
                ct_contrib = ct_contrib.simplify()
            if ct_contrib == 0 and pc.get('warn', True):
                logger.warning(f"Counterterm contribution of {ctname} to {(h1,h2,h3)} vanishes (set `warn=False` for this CT to disable this message)")
            logging.debug(f'result: {ct_contrib}')
            customren += self._eval(str(ct_contrib), **all_cts)

        proc = sorted([h1.name,h2.name,h3.name])

        # legacy of v1.0 notation for specifying CTs
        old_ct = self.scheme.get('custom_CT_hhh', None)
        if old_ct and proc not in [c['amplitude'] for c in self.scheme['custom_counterterms']]:
            logger.warning('The "cumston_CT_hhh" notation is deprecated! converting to the new convention...')
            self.scheme['custom_counterterms'].append({'amplitude': proc, 'condition': old_ct})

        # contrubtions from user defined vertex counterterms
        for ct in self.scheme['custom_counterterms']:
            amplitude = sorted(ct.get('amplitude',[]))
            if not amplitude:
                logger.error(f'Need to provide "amplitude" for custom CT. E.g. amplitude: [h1,h1,h1] (CT: {ct})"')
                continue
            if amplitude != proc:
                continue
            custom_ct_str = ct.get('condition', None)
            if not custom_ct_str:
                logger.error(f'No "condition" provided for {amplitude} counterterm')
                continue

            logger.info('calculate custom CT contribution to lambda_hhh')
            CT = ''
            # prepare a local environment with sympy expressions for the evaluation of custom counterterms
            locs = {}
            locs.update(locals())
            locs.update(self.symbols_values)
            locs.update(self.symbols)

            try:
                exec(custom_ct_str, globals(), locs)
            except Exception as e:
                raise Exception("CounterTermError", "could not execute your custom counter term due to following error: \n" + str(e))
            CT = locs.get('customCT', '')
            # return value of v1.0 legacy
            if hasattr(self,'custom_CT_hhh') and CT == '':
                CT = self.custom_CT_hhh

            if CT == '':
                logger.error('your custom counterterm did not store a condition in the variable `CT`')
            try:
                CT = self._eval(str(CT))
            except Exception as e:
                logger.error("could not calculate custom counter term due to following error: \n" + str(e))
                CT = ''
            if CT == 0:
                logger.warning('your custom counter term evaluated to zero (0)!')
            customren += CT

        result = {'VEV_CT': vevren, 'mass_CTs': massren, 'customCT': customren}

        if not only_topologies and self.evaluation != 'numerical':
            logger.debug(f'Writing results to cache ({CTfile}).')
            with open(CTfile, 'w') as f:
                json.dump(result, f)
            lock.release()
        if firstrun:
            self.set_evaluation_mode(evalSAVE)
            if self.evaluation == 'numerical':
                momenta_d = {k: self._eval(v) for k,v, in momenta_d.items()}
                for k,v in result.items():
                    try:
                        result[k] = self._eval(v, **momenta_d)
                    except Exception as e:
                        raise Exception(f"There was an error when evaluating the '{k}'-counterterm contribution") from e

            elif self.evaluation == 'analytical':
                for r in result:
                    for m,md in momenta_d.items():
                        result[r] = result[r].replace(m, str(md))
                    for name,c in reversed(self.couplings.items()):
                        result[r] = result[r].replace(name, f'({c.value})')

        return result
