#
# Copyright (C) 2019 Sylvain Marsat, John G. Baker.
#
#


"""
    Python functions for LISA waveforms and TDI observables.
"""


import numpy as np
import copy

import lisabeta
import lisabeta.pyconstants as pyconstants
import lisabeta.tools.pytools as pytools
import lisabeta.tools.pyspline as pyspline
import lisabeta.tools.pyoverlap as pyoverlap
import lisabeta.lisa.pyresponse as pyresponse
import lisabeta.lisa.pyLISAnoise as pyLISAnoise
import lisabeta.lisa.lisatools as lisatools
import lisabeta.waveforms.bbh.pyIMRPhenomD as pyIMRPhenomD
import lisabeta.waveforms.bbh.pyIMRPhenomHM as pyIMRPhenomHM
import lisabeta.waveforms.bbh.pyEOBNRv2HMROM as pyEOBNRv2HMROM
#import lisabeta.likelihood.likelihood as likelihood
#import matplotlib.pyplot as plt


################################################################################

class SignalEmptyValueError(ValueError): pass

################################################################################

# List of supported approximants
list_approximants_smbh = ['IMRPhenomD', 'IMRPhenomHM', 'EOBNRv2HMROM']
list_approximants_sobh = ['IMRPhenomD']

# Default waveform params
waveform_params_smbh_default = {
    "minf": 1e-5,
    "maxf": 0.5,
    "t0": 0.0,
    "timetomerger_max": 1.0,
    "fend": None,
    "tmin": None,
    "tmax": None,
    "phiref": 0.0,
    "fref_for_phiref": 0.0,
    "tref": 0.0,
    "fref_for_tref": 0.0,
    "force_phiref_fref": True,
    "toffset": 0.0,
    "modes": None,
    "TDI": "TDIAET",
    "acc": 1e-4,
    "order_fresnel_stencil": 0,
    "approximant": "IMRPhenomD",
    "LISAconst": "Proposal",
    "responseapprox": "full",
    "frozenLISA": False,
    "TDIrescaled": True,
    "LISAnoise": {
        "InstrumentalNoise": "SciRDv1",
        "WDbackground": False,
        "WDduration" : 0.0,
        "lowf_add_pm_noise_f0": 0.0,
        "lowf_add_pm_noise_alpha": 2.0
    }
}
waveform_params_sobh_default = {
    "minf": 1e-5,
    "maxf": 0.5,
    "fend": None,
    "tstart": 0.0,
    "tmin": 0.0,
    "tmax": 4.0,
    "phiref": 0.0,
    "fref_for_phiref": None,
    "toffset": 0.0,
    "TDI": "TDIAET",
    "acc": 1e-4,
    "order_fresnel_stencil": 0,
    "approximant": "IMRPhenomD",
    "LISAconst": "Proposal",
    "responseapprox": "full",
    "frozenLISA": False,
    "TDIrescaled": True,
    "LISAnoise": {
        "InstrumentalNoise": "SciRDv1",
        "WDbackground": False,
        "WDduration" : 0.0,
        "lowf_add_pm_noise_f0": 0.0,
        "lowf_add_pm_noise_alpha": 2.0
    }
}

################################################################################

def FrequencyBoundsLISATDI_SMBH(params, timetomerger_max=1., minf=1e-5, maxf=1., fstart22=None, fend22=None, tmin=None, tmax=None, Mfmax_model=0.2, DeltatL_cut=None, DeltatSSB_cut=None, t0=0., tref=0., phiref=0., fref_for_phiref=0., fref_for_tref=0., force_phiref_fref=True, toffset=0., frozenLISA=False, scale_freq_hm=True, modes=None, approximant='IMRPhenomD', **kwargs):
    """Helper function to get the (2,2) frequency bounds in GenerateLISATDI_SMBH
    Args:
      params              # Dictionary of input parameters for the signal
                            Can be in SSB-frame or L-frame, if the latter it
                            will be converted back to SSB-frame
      Params dictionary keys format:
       [m1,               # Redshifted mass of body 1 (solar masses)
        m2,               # Redshifted mass of body 2 (solar masses)
        chi1,             # Dimensionless spin of body 1 (in [-1, 1])
        chi2,             # Dimensionless spin of body 2 (in [-1, 1])

        Deltat,           # Time shift (s)
        dist,             # Luminosity distance (Mpc)
        inc,              # Inclination angle (rad)
        phi,              # Observer's azimuthal phase (rad)
        lambda,           # Source longitude (rad)
        beta,             # Source latitude (rad)
        psi]              # Polarization angle (rad)

    Keyword args:
      timetomerger_max    # Time to merger (yr) to set an fstart cutting the
                            waveform (None to ignore, default 1.)
      minf                # Minimal frequency (Hz)
      maxf                # Maximal frequency (Hz)
      fstart22            # Starting frequency for 22 mode (Hz) (None to ignore)
      fend22              # Ending frequency for 22 mode (Hz) (None to ignore)
      tmin                # Starting time of observation window (yr),
                            SSB time absolute (default None, ignore)
      tmax                # Ending time of observation window (yr),
                            SSB time absolute (default None, ignore)
      Mfmax_model         # Max geometric frequency generated by the waveform
                            model (default 0.2) -- for 22, scaled for HM if
                            scale_freq_hm
      DeltatL_cut         # Ending time of observations (s), NOTE: L-frame
                            time measured from t0 (default None, ignore)
      DeltatSSB_cut       # [For testing only] Ending time of observations (s),
                            NOTE: SSB-frame time measured from t0
                            (default None,ignore)
      scale_freq_hm       # Scale freq cuts by m/2 (default True)
                            If False, compute f_lm(t) by inversion separately
                            the output will then be a dictionary across modes
      modes               # Set of modes, used when scale_freq_hm is False
      t0                  # Reference time (yr), so that t=0 for the waveform
                            corresponds to t0 in the SSB-frame
      tref                # Time at fref_for_tref (s) (default 0)
      phiref              # Orbital phase at fref_for_phiref (rad) (default 0)
      fref_for_tref       # Ref. frequency (Hz) for tref (default 0 for fpeak)
      fref_for_phiref     # Ref. frequency (Hz) for phiref (default 0 for fpeak)
      force_phiref_fref   # Flag to force phiref at fref (default True)
      toffset             # Extra time shift applied to the waveform (s)
      frozenLISA          # Freeze LISA motion
    """

    # If input parameters are given in the Lframe, convert to SSBframe
    if params.get('Lframe', False):
        params = lisatools.convert_Lframe_to_SSBframe(params,
                                                      t0=t0,
                                                      frozenLISA=frozenLISA)

    params = pytools.complete_mass_params(params)
    params = pytools.complete_spin_params(params)

    m1 = params['m1']
    m2 = params['m2']
    chi1 = params['chi1']
    chi2 = params['chi2']
    Deltat = params['Deltat']
    dist = params['dist']
    inc = params['inc']
    phi = params['phi']
    lambd = params['lambda']
    beta = params['beta']
    psi = params['psi']

    # Units
    M = m1 + m2
    Ms = M * pyconstants.MTSUN_SI

    # Set of harmonics to be returned, default for approximants
    # TODO: add error raising if incompatibility with mode content of approx.
    if modes is None:
        if approximant=='IMRPhenomD':
            modes = [(2,2)]
        elif approximant=='IMRPhenomHM':
            modes = [(2,2), (2,1), (3,3), (3,2), (4,4), (4,3)]
        elif approximant=='EOBNRv2HMROM':
            modes = [(2,2), (2,1), (3,3), (4,4), (5,5)]

    # Check logic of the arguments
    # Presence of higher harmonics
    hm = (not (modes==[(2,2)]))
    # Presence of a time cut with numerical inversion f(t)
    time_cut_foft = (tmin is not None) or (tmax is not None) or (DeltatL_cut is not None) or (DeltatSSB_cut is not None)
    # If True result will be separate as a dict across modes
    separate_hm = hm and not scale_freq_hm
    # Presence of a cut in 22-mode frequency
    f22_cut = ((not fstart22 is None) or (not fend22 is None))
    # NOTE: when HM are present, cut in f22 and cut in time incompatible, unless using scaling relation m/2*f22
    if f22_cut and not scale_freq_hm:
        raise ValueError('Cut in f22 can only be used with the scaling relation m/2*f22, no input for separate flm yet.')

    # Will remain a number if not separate_hm, else will be a dict over modes
    # We start from minf, maxf and will update the bounds
    # We will come back and re-enforce minf,maxf across all modes at the end
    fLow = minf
    fHigh = maxf

    # Maximal frequency covered by the waveform model
    fHigh = np.fmin(fHigh, Mfmax_model / Ms)

    # Cuts expressed directly in the 22-mode frequency
    # minf, maxf are global across modes while fstart22, fend22 will be scaled
    # Incompatible with separate_hm for now (need input fstart_lm, fend_lm)
    if not fstart22 is None:
        fLow = np.fmax(fLow, fstart22)
    if not fend22 is None:
        fHigh = np.fmin(fHigh, fend22)

    # Newtonian estimate for the starting frequency given a max duration of signal
    # If output is going to be separate for each mode, scale this by m/2
    if timetomerger_max is not None:
        f_timetomerger_max = pytools.funcNewtonianfoft(m1, m2, timetomerger_max * pyconstants.YRSID_SI)
        fLow = np.fmax(f_timetomerger_max, fLow)

    # Take into account time cuts if specified - we need a mock waveform class for f(t) inversion
    # TODO: this repeats the initialization of the waveform, not optimal
    # NOTE: for now, here we use PhenomD/HM regardless of the approximant asked for
    # NOTE: PhenomD has more alignment options than PhenomHM, risk for inconsistency
    if time_cut_foft:
        f_t_acc = 1e-6 # Hardcoded accuracy of f(t) function, for t in s
        mock_gridfreq = np.array([fLow, fHigh])
        if (not hm) or scale_freq_hm:
            mock_wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(mock_gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=force_phiref_fref, Deltat=Deltat)
        else:
            mock_wfClass = pyIMRPhenomHM.IMRPhenomHMhlmAmpPhase(mock_gridfreq, m1, m2, chi1, chi2, dist, phiref=phiref, fref=fref_for_phiref, Deltat=Deltat)
        mock_tpeak = mock_wfClass.get_tpeak()
        mock_fpeak = mock_wfClass.get_fpeak()

    # At this stage fLow,fHigh are still single numbers
    # After this fLow,fHigh are either single numbers or a dict depending on separate_hm
    if separate_hm:
        fLow = dict([(lm, lm[1]/2. * fLow) for lm in modes])
        fHigh = dict([(lm, lm[1]/2. * fHigh) for lm in modes])

    # NOTE: times in the waveform are relative to t0, hence [tmin-t0, tmax-t0]
    # NOTE: DeltatL_cut is given in the L-frame, convert to SSB-frame
    # NOTE: DeltatSSB_cut in SSB-frame, for testing only (not physical)
    Deltatmin_s = None
    Deltatmax_s = None
    if tmin is not None:
        Deltatmin_s = (tmin - t0) * pyconstants.YRSID_SI
    if (tmax is not None) or (DeltatL_cut is not None) or (DeltatSSB_cut is not None):
        Deltatmax_s = np.infty
        if tmax is not None:
            Deltatmax_s = np.fmin(Deltatmax_s, (tmax-t0) * pyconstants.YRSID_SI)
        if (DeltatL_cut is not None):
            if (DeltatSSB_cut is not None):
                raise ValueError('DeltatL_cut and DeltatSSB_cut are exclusive.')
            tL_cut = t0*pyconstants.YRSID_SI + DeltatL_cut
            Deltat_cut = lisatools.tSSBfromLframe(tL_cut, lambd, beta, frozenLISA=frozenLISA, tfrozenLISA=t0) - t0*pyconstants.YRSID_SI
            Deltatmax_s = np.fmin(Deltatmax_s, Deltat_cut)
        if (DeltatSSB_cut is not None):
            if (DeltatL_cut is not None):
                raise ValueError('DeltatL_cut and DeltatSSB_cut are exclusive.')
            Deltatmax_s = np.fmin(Deltatmax_s, DeltatSSB_cut)

    # Compute f(t) for time cut at the beginning
    if Deltatmin_s is not None:
        if not separate_hm:
            fLow_tmin = mock_wfClass.compute_foft(Deltatmin_s, fLow, f_t_acc)
            fLow = np.fmax(fLow, fLow_tmin)
        else:
            tf_lm = dict([(lm, Deltatmin_s) for lm in modes])
            fLow_tmin_lm = mock_wfClass_hm.compute_foft_lm(tf_lm, fLow, f_t_acc)
            fLow = dict([(lm, np.fmax(fLow_tmin_lm[lm], fLow[lm])) for lm in modes])

    # Compute f(t) for time cut at the end
    # NOTE: the cut in tmax is ignored if tmax > tpeak
    if Deltatmax_s is not None and (Deltatmax_s < mock_tpeak):
        fHigh_tmax_guess = pytools.funcNewtonianfoft(m1, m2, mock_tpeak - Deltatmax_s)
        fHigh_tmax_guess = np.fmin(fHigh_tmax_guess, mock_fpeak)
        if not separate_hm:
            fHigh_tmax = mock_wfClass.compute_foft(Deltatmax_s, fHigh_tmax_guess, f_t_acc)
            fHigh = np.fmin(fHigh, fHigh_tmax)
        else:
            tf_lm = dict([(lm, Deltatmax_s) for lm in modes])
            fHigh_tmax_lm = mock_wfClass.compute_foft_lm(tf_lm, fHigh_tmax_guess, f_t_acc)
            fHigh = dict([(lm, np.fmin(fHigh_tmax_lm[lm], fHigh[lm])) for lm in modes])

    if not separate_hm:
        if fLow >= fHigh:
            print("fLow > fHigh [%f > %f]"%(fLow, fHigh))
            raise SignalEmptyValueError
    else:
        if fLow[(2,2)] >= fHigh[(2,2)]:
            print("fLow > fHigh [%f > %f]"%(fLow, fHigh))
            raise SignalEmptyValueError

    # Re-enforce minf, maxf limits when separating modes
    if separate_hm:
        fLow = dict([(lm, np.fmax(minf, fLow[lm])) for lm in modes])
        fHigh = dict([(lm, np.fmin(maxf, fHigh[lm])) for lm in modes])

    return fLow, fHigh

def GenerateLISATDI_SMBH(params, timetomerger_max=1., minf=1e-5, maxf=1., fstart22=None, fend22=None, tmin=None, tmax=None, Mfmax_model=0.2, DeltatL_cut=None, DeltatSSB_cut=None, scale_freq_hm=True, gridfreq=None, t0=0., tref=0., phiref=0., fref_for_phiref=0., fref_for_tref=0., force_phiref_fref=True, toffset=0., tf_method=None, modes=None, TDI='TDIAET', acc=1e-4, order_fresnel_stencil=0, approximant='IMRPhenomD', LISAconst=pyresponse.LISAconstProposal, responseapprox='full', frozenLISA=False, TDIrescaled=True, extra_params=None, **kwargs):
    """Main function to generate LISA SMBH signals
    Args:
      params              # Dictionary of input parameters for the signal
                            Can be in SSB-frame or L-frame, if the latter it
                            will be converted back to SSB-frame
      Params dictionary keys format:
       [m1,               # Redshifted mass of body 1 (solar masses)
        m2,               # Redshifted mass of body 2 (solar masses)
        chi1,             # Dimensionless spin of body 1 (in [-1, 1])
        chi2,             # Dimensionless spin of body 2 (in [-1, 1])

        Deltat,           # Time shift (s)
        dist,             # Luminosity distance (Mpc)
        inc,              # Inclination angle (rad)
        phi,              # Observer's azimuthal phase (rad)
        lambda,           # Source longitude (rad)
        beta,             # Source latitude (rad)
        psi]              # Polarization angle (rad)

    Keyword args:
      timetomerger_max    # Time to merger (yr) to set an fstart cutting the
                            waveform (None to ignore, default 1.)
      minf                # Minimal frequency (Hz)
      maxf                # Maximal frequency (Hz)
      fstart22            # Starting frequency for 22 mode (Hz) (None to ignore)
      fend22              # Ending frequency for 22 mode (Hz) (None to ignore)
      tmin                # Starting time of observation window (yr),
                            SSB time absolute (default None, ignore)
      tmax                # Ending time of observation window (yr),
                            SSB time absolute (default None, ignore)
      Mfmax_model         # Max geometric frequency generated by the waveform
                            model (default 0.2) -- for 22, scaled for HM if
                            scale_freq_hm
      DeltatL_cut         # Ending time of observations (s), NOTE: L-frame
                            time measured from t0 (default None, ignore)
      DeltatSSB_cut       # [For testing only] Ending time of observations (s),
                            NOTE: SSB-frame time measured from t0
                            (default None,ignore)
      scale_freq_hm       # Scale freq cuts by m/2 (default True)
                            If False, compute f_lm(t) by inversion separately
      gridfreq            # Frequencies on which to generate, numpy ndarray
                            (default None, ignore); overrides other time and
                            frequency bounds.
                            If scale_freq_hm is True, scaled by m/2 for modes.
                            For Fisher, allows to force the use of the same
                            gridfreq for numerical derivatives (interpolation
                            errors important otherwise)
      t0                  # Reference time (yr), so that t=0 for the waveform
                            corresponds to t0 in the SSB-frame
      tref                # Time at fref_for_tref (s) (default 0)
      phiref              # Orbital phase at fref_for_phiref (rad) (default 0)
      fref_for_tref       # Ref. frequency (Hz) for tref (default 0 for fpeak)
      fref_for_phiref     # Ref. frequency (Hz) for phiref (default 0 for fpeak)
      force_phiref_fref   # Flag to force phiref at fref (default True)
      toffset             # Extra time shift applied to the waveform (s)
      tf_method           # Method to compute tf, spline or analytical
                            analytical only available for some approx.
      modes               # Set of modes to generate, None for default
      TDI                 # Choice of TDI channels (default TDIAET)
      acc                 # Target interpolation accuracy for inspiral
      order_fresnel_stencil # Order of Fresnel stencil in response
      approximant         # Waveform approximant
      LISAconst           # LISA constellation constants
      responseapprox      # Approximation in the response (full, lowfL, lowf)
      frozenLISA          # Freeze LISA motion
      TDIrescaled         # Rescale TDI variables (by prefactor common in TDI
                            and noise)
      extra_params        # Dictionary of additional parameters
      |- use_buggy_LAL_tpeak # Reproduce bug in tpeak alignment in LAL
                             (default False)
    """

    # If input parameters are given in the Lframe, convert to SSBframe
    if params.get('Lframe', False):
        params = lisatools.convert_Lframe_to_SSBframe(params,
                                                      t0=t0,
                                                      frozenLISA=frozenLISA)

    params = pytools.complete_mass_params(params)
    params = pytools.complete_spin_params(params)

    m1 = params['m1']
    m2 = params['m2']
    chi1 = params['chi1']
    chi2 = params['chi2']
    Deltat = params['Deltat']
    dist = params['dist']
    inc = params['inc']
    phi = params['phi']
    lambd = params['lambda']
    beta = params['beta']
    psi = params['psi']

    # Check args
    if not (approximant in list_approximants_smbh):
        raise ValueError('Approximant %s not supported yet.' % (approximant))
    if not (TDI=='TDIAET' or TDI=='TDI2AET'):
        raise ValueError('Only TDI=TDIAET, TDI2AET implemented for now.')
    if not order_fresnel_stencil==0:
        raise ValueError('Only order_fresnel_stencil=0 implemented for now.')

    # Set of harmonics to be returned, default for approximants
    # TODO: add error raising if incompatibility with mode content of approx.
    if modes is None:
        if approximant=='IMRPhenomD':
            modes = [(2,2)]
        elif approximant=='IMRPhenomHM':
            modes = [(2,2), (2,1), (3,3), (3,2), (4,4), (4,3)]
        elif approximant=='EOBNRv2HMROM':
            modes = [(2,2), (2,1), (3,3), (4,4), (5,5)]

    # Units
    q = m1 / m2
    eta = pytools.etaofq(q)
    M = m1 + m2
    Ms = M * pyconstants.MTSUN_SI

    # If the user specified gridfreq, force to use it -- overriding other bounds
    # in frequency (e.g. minf, maxf) or time (e.g. DeltatL_cut)
    input_gridfreq = not gridfreq is None
    if not input_gridfreq:
        # Determine frequency bounds based on frequency and time limits
        fLow, fHigh = FrequencyBoundsLISATDI_SMBH(params, t0=t0, timetomerger_max=timetomerger_max, minf=minf, maxf=maxf, fstart22=fstart22, fend22=fend22, tmin=tmin, tmax=tmax, Mfmax_model=Mfmax_model, DeltatL_cut=DeltatL_cut, DeltatSSB_cut=DeltatSSB_cut, scale_freq_hm=scale_freq_hm, modes=modes, approximant=approximant)

        # Combined frequency grid for the waveform
        # If HM and not using m/2 scaling, do log-affine scaling
        # with frequency bounds for individual modes
        if isinstance(fLow, dict) and isinstance(fHigh, dict):
            gridfreqClass = pytools.FrequencyGrid(fLow[(2,2)], fHigh[(2,2)], M, q, acc=acc)
            gridfreq22 = gridfreqClass.get_freq()
            gridfreq = {}
            for lm in modes:
                gridfreq[lm] = pytools.log_affine_scaling(gridfreq22, fLow[lm], fHigh[lm])
        else:
            # For PhenomHM will be rescaled by m/2 for different modes hlm
            gridfreqClass = pytools.FrequencyGrid(fLow, fHigh, M, q, acc=acc)
            gridfreq = gridfreqClass.get_freq()
    else:
        # NOTE: when loading from json, gridfreq will be a list
        # gridfreq can also be a dictionary of numpy arrays, one for each mode
        if isinstance(gridfreq, list):
            gridfreq = np.array(gridfreq)
        if (approximant=='EOBNRv2HMROM'):
            raise ValueError('Input gridfreq not supported for EOBNRv2HMROM.')

    # Generate waveform
    if (approximant=='IMRPhenomD'):
        # IMRPhenomD
        # Specifying fref_for_phiref, phiref defines the source frame
        # tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
        wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=force_phiref_fref, Deltat=Deltat, extra_params=extra_params)
        wfhlm = wfClass.get_waveform()
        fpeak = wfClass.get_fpeak()
    elif (approximant=='IMRPhenomHM'):
        # IMRPhenomHM
        # For now tf=0 at fpeak hardcoded
        # fref means fref_for_phiref, default fpeak
        wfClass = pyIMRPhenomHM.IMRPhenomHMhlmAmpPhase(gridfreq, m1, m2, chi1, chi2, dist, phiref=phiref, fref=fref_for_phiref, Deltat=Deltat, scale_freq_hm=scale_freq_hm, extra_params=extra_params)
        wfhlm = wfClass.get_waveform()
        fpeak = wfClass.get_fpeak()
    elif (approximant=='EOBNRv2HMROM'):
        # EOBNRv2HMROM
        # NOTE: ROM output on its own frequencies, not gridfreq
        # For now tf=0 at fpeak hardcoded
        # fref means fref_for_phiref, default fpeak
        # Remnants from LAL and its SI units...
        if not (chi1==0. and chi2==0.):
            raise ValueError('Spins not supported for EOBNRv2HMROM.')
        m1_SI = m1 * pyconstants.MSUN_SI
        m2_SI = m2 * pyconstants.MSUN_SI
        dist_SI = dist * 1e6*pyconstants.PC_SI
        f_min = fLow
        deltatRef = Deltat
        fref = fref_for_phiref
        wfClass = pyEOBNRv2HMROM.EOBNRv2HMROM(m1_SI, m2_SI, dist_SI, f_min, deltatRef, phiref, fref, modes=modes)
        wfhlmEOB = wfClass.get_waveform()
        # Output format freq_amp/amp_real/amp_imag/freq_phase/phase
        # but freqs actually the same and amp_imag=0
        wfhlm = {}
        for lm in modes:
            wfhlm[lm] = {}
            wfhlm[lm]['freq'] = wfhlmEOB[lm]['freq_phase']
            wfhlm[lm]['amp'] = wfhlmEOB[lm]['amp_real']
            wfhlm[lm]['phase'] = wfhlmEOB[lm]['phase']

    # Cut waveform modes after rescaling to enforce [minf, maxf]
    # Ignore if using an input gridfreq
    # TODO: this cut is coarse, due to the discreteness of spline points
    if not input_gridfreq:
        for lm in modes:
            mask = np.logical_and(minf <= wfhlm[lm]['freq'], wfhlm[lm]['freq'] <= maxf)
            wfhlm[lm]['freq'] = wfhlm[lm]['freq'][mask].copy()
            wfhlm[lm]['amp'] = wfhlm[lm]['amp'][mask].copy()
            wfhlm[lm]['phase'] = wfhlm[lm]['phase'][mask].copy()
            if 'tf' in wfhlm[lm].keys():
                wfhlm[lm]['tf'] = wfhlm[lm]['tf'][mask].copy()

    # Compute tf
    if tf_method is None:
        if (approximant=='IMRPhenomD' or approximant=='IMRPhenomHM'):
            tf_method = 'analytical'
        elif (approximant=='EOBNRv2HMROM'):
            tf_method = 'spline'
    tf = {}
    if tf_method=='spline':
        tfspline = {}
        for lm in modes:
            phaseov2pisplineClass = pyspline.CubicSpline(wfhlm[lm]['freq'], wfhlm[lm]['phase'] / (2*np.pi))
            tfspline[lm] = phaseov2pisplineClass.get_spline_d()
            # For a given mode, array can be empty due to time/frequency cuts
            if len(tfspline[lm])==0:
                tf[lm] = np.empty(0, dtype=float)
            else:
                tf[lm] = tfspline[lm][:,1].copy()
    elif tf_method=='analytical':
        for lm in modes:
            tf[lm] = wfhlm[lm]['tf'].copy()

    # Apply an extra time offset (formerly tref, used for John's likelihood)
    for lm in modes:
        wfhlm[lm]['phase'] += 2*np.pi * wfhlm[lm]['freq'] * toffset
        tf[lm] += toffset

    # Compute TDI response
    phaseRdelay = {}
    transferL1 = {}
    transferL2 = {}
    transferL3 = {}
    for lm in modes:
        l = lm[0]
        m = lm[1]
        # TODO: is this check necessary now with a tf vector ?
        nsize = len(wfhlm[lm]['freq'])
        if len(tf[lm])==0 and nsize>0:
            #Note this is something of a hack. Addressing the edge case where CubicSpline fails.  Cannot compute response, so setting to 0
            print("GenerateLISATDI_SMBH: WARNING! Array too small, n=",nsize,"for lm=",lm,". Transfer function undetermined, setting to vanish.")
            phaseRdelay[lm] = np.zeros(nsize)
            transferL1[lm] = np.zeros(nsize)
            transferL2[lm] = np.zeros(nsize)
            transferL3[lm] = np.zeros(nsize)
        else:
            tdiClass = pyresponse.LISAFDresponseTDI3Chan(wfhlm[lm]['freq'], tf[lm], t0, l, m, inc, phi, lambd, beta, psi, TDI=TDI, LISAconst=LISAconst, responseapprox=responseapprox, frozenLISA=frozenLISA, TDIrescaled=TDIrescaled)
            phaseRdelay[lm], transferL1[lm], transferL2[lm], transferL3[lm] = tdiClass.get_response()

    # Output
    wftdi = {}
    wftdi['params'] = params.copy()
    wftdi['modes'] = list(modes)
    if (approximant=='IMRPhenomD' or approximant=='IMRPhenomHM'):
        wftdi['fpeak'] = fpeak
    for lm in modes:
        wftdi[lm] = {}
        wftdi[lm]['freq'] = wfhlm[lm]['freq']
        wftdi[lm]['amp'] = wfhlm[lm]['amp']
        wftdi[lm]['phase'] = wfhlm[lm]['phase']
        wftdi[lm]['phaseRdelay'] = phaseRdelay[lm]
        wftdi[lm]['transferL1'] = transferL1[lm]
        wftdi[lm]['transferL2'] = transferL2[lm]
        wftdi[lm]['transferL3'] = transferL3[lm]
        wftdi[lm]['tf'] = tf[lm]

    return wftdi

def GenerateLISATDI_SOBH(params, minf=1e-5, maxf=0.5, fend=None, tstart=0., tmin=None, tmax=None, fref_for_phiref=None, phiref=0., toffset=0., TDI='TDIAET', acc=1e-4, order_fresnel_stencil=0, approximant='IMRPhenomD', LISAconst=pyresponse.LISAconstProposal, responseapprox='full', frozenLISA=False, TDIrescaled=True, **kwargs):
    """m1, m2, chi1, chi2, fstart, dist, inc, phi, lambd, beta, psi
    Args:
      params              # Dictionary of input parameters for the signal
      Params dictionary keys format:
       [m1,               # Redshifted mass of body 1 (solar masses)
        m2,               # Redshifted mass of body 2 (solar masses)
        chi1,             # Dimensionless spin of body 1 (in [-1, 1])
        chi2,             # Dimensionless spin of body 2 (in [-1, 1])

        fstart,           # Frequency at the start of the observations (Hz)
        dist,             # Luminosity distance (Mpc)
        inc,              # Inclination angle (rad)
        phi,              # Observer's azimuthal phase (rad)
        lambda,           # Source longitude in SSB-frame (rad)
        beta,             # Source latitude in SSB-frame (rad)
        psi]              # Polarization angle (rad)

    Keyword args:
      minf                # Minimal frequency (Hz) (default 1e-5Hz)
      maxf                # Maximal frequency (Hz) (default 0.5Hz)
      fend                # Ending frequency for 22 mode (Hz) (None to ignore)
      tstart              # Starting time where f=fstart (yr) (default 0.)
      tmin                # Starting time of observation window (yr) (default None, tstart)
      tmax                # Ending time of observation window (yr) (default None, ignore)
      phiref              # Ref. phase (rad) at fref_for_phiref (default 0.)
      fref_for_phiref     # Ref. frequency for phase (Hz) (default None for fstart, set to 0 for fpeak)
                            NOTE: tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
                            NOTE: with tref=0, force_phiref_fref has no incidence
      toffset             # Extra time shift applied to the waveform (s)
      TDI                 # Choice of TDI channels (default TDIAET)
      acc                 # Target interpolation accuracy for inspiral
      order_fresnel_stencil # Order of Fresnel stencil in response
      approximant         # Waveform approximant
      LISAconst           # LISA constellation constants
      responseapprox      # Approximation in the response (full, lowfL, lowf)
      frozenLISA          # Freeze LISA motion
      TDIrescaled         # Rescale TDI variables (by prefactor common in TDI
                            and noise)
    """

    params = pytools.complete_mass_params(params)
    params = pytools.complete_spin_params(params)

    m1 = params['m1']
    m2 = params['m2']
    chi1 = params['chi1']
    chi2 = params['chi2']
    fstart = params['fstart']
    dist = params['dist']
    inc = params['inc']
    phi = params['phi']
    lambd = params['lambda']
    beta = params['beta']
    psi = params['psi']
    if params.get('Lframe', False):
        raise ValueError('Source params given in the L-frame, not implemented.')

    # Check args
    if not (approximant in list_approximants_sobh):
        raise ValueError('Approximant %s not supported yet.' % (approximant))
    if not (TDI=='TDIAET' or TDI=='TDI2AET'):
        raise ValueError('Only TDI=TDIAET, TDI2AET implemented for now.')
    if not order_fresnel_stencil==0:
        raise ValueError('Only order_fresnel_stencil=0 implemented for now.')

    # Units
    q = m1 / m2
    #eta = pytools.etaofq(q)
    M = m1 + m2
    Ms = M * pyconstants.MTSUN_SI
    #Mfmin = Ms * minf
    Mfmax = Ms * maxf
    Mfmax_model = 0.2
    Mfmax = np.fmin(Mfmax, Mfmax_model)
    maxf = Mfmax / Ms

    # Deal with arguments
    fLow = np.fmax(fstart, minf)
    fHigh = maxf
    if fend is not None: fHigh = np.fmin(fHigh, fend)

    # Setting fref for time and phase
    # tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
    tref = 0.
    fref_for_tref = fstart
    # Specifying fref_for_phiref, phiref defines the source frame
    # If fref_for_phiref is None (default), use fstart
    if fref_for_phiref is None: fref_for_phiref = fstart
    # No Deltat allowed in principle, since we set the time at fstart
    Deltat = 0.

    # Take into account time cuts if specified
    # TODO: this repeats the initialization of the waveform, not optimal
    # NOTE: the cut in tmax is ignored if tmax > tpeak
    if (tmin is not None) or (tmax is not None):
        f_t_acc = 1e-1 # Hardcoded accuracy of f(t) function, for t in s
        mock_gridfreq = np.array([fLow, fHigh])
        mock_wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(mock_gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=False, Deltat=Deltat)
        mock_tpeak = mock_wfClass.get_tpeak()
        if tmin is not None:
            tmin_s = tmin * pyconstants.YRSID_SI
            fLow_tmin = mock_wfClass.compute_foft(tmin_s, fLow, f_t_acc)
            fLow = np.fmax(fLow, fLow_tmin)
        if tmax is not None:
            tmax_s = tmax * pyconstants.YRSID_SI
            if tmax_s < mock_tpeak:
                fHigh_tmax_guess = pytools.funcNewtonianfoft(m1, m2, mock_tpeak - tmax_s)
                fHigh_tmax = mock_wfClass.compute_foft(tmax_s, fHigh_tmax_guess, f_t_acc)
                if fHigh_tmax!=0:
                    fHigh = np.fmin(fHigh, fHigh_tmax)

    # Combined frequency grid for the waveform
    if fLow >= fHigh:
        print("fLow > fHigh [%f > %f]"%(fLow, fHigh))
        raise ValueError
    gridfreqClass = pytools.FrequencyGrid(fLow, fHigh, M, q, acc=acc)
    gridfreq = gridfreqClass.get_freq()

    # Generate IMRPhenomD waveform
    wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=False, Deltat=Deltat)
    #FIXME  This has been adapted by John following Sylvain;s changes to waveform interface to support HM
    wfhlm = wfClass.get_waveform()
    freq = wfhlm[(2,2)]['freq']
    amp = wfhlm[(2,2)]['amp']
    phase = wfhlm[(2,2)]['phase']
    tf = wfhlm[(2,2)]['tf']
    fpeak = wfClass.get_fpeak()

    # Apply an extra time offset (formerly tref, used for John's likelihood)
    phase += 2 * np.pi * freq * toffset
    tf += toffset

    # # Build spline for tf
    # phaseov2pisplineClass = pyspline.CubicSpline(freq, phase / (2*np.pi))
    # tfspline = phaseov2pisplineClass.get_spline_d()
    # tf = tfspline[:,1].copy()

    # tf=0 in the waveform corresponds to t0 in SSB time
    # We therefore fix t0 to tstart
    t0 = tstart

    # Compute TDI response
    l = 2
    m = 2
    tdiClass = pyresponse.LISAFDresponseTDI3Chan(gridfreq, tf, t0, l, m, inc, phi, lambd, beta, psi, TDI=TDI, LISAconst=LISAconst, responseapprox=responseapprox, frozenLISA=frozenLISA, TDIrescaled=TDIrescaled)
    phaseRdelay, transferL1, transferL2, transferL3 = tdiClass.get_response()

    # Output
    wftdi = {}
    wftdi['params'] = params.copy()
    wftdi['fpeak'] = fpeak
    wftdi['modes'] = [(2,2)]
    wftdi[(2,2)] = {}
    wftdi[(2,2)]['freq'] = freq
    wftdi[(2,2)]['amp'] = amp
    wftdi[(2,2)]['phase'] = phase
    wftdi[(2,2)]['phaseRdelay'] = phaseRdelay
    wftdi[(2,2)]['transferL1'] = transferL1
    wftdi[(2,2)]['transferL2'] = transferL2
    wftdi[(2,2)]['transferL3'] = transferL3
    wftdi[(2,2)]['tf'] = tf

    return wftdi

# TODO: created to have access to h22td from inverse spa, duplicates a lot of code
def Generateh22TD_SOBH(params, minf=1e-5, maxf=0.5, fend=None, tstart=0., tmin=None, tmax=None, fref_for_phiref=None, phiref=0., toffset=0., acc=1e-4, approximant='IMRPhenomD', **kwargs):
    """Generate time-domain h22 from PhenomD and inverse SPA (inspiral only)
    Args:
      params              # Dictionary of input parameters for the signal
      Keys: m1, m2, chi1, chi2, fstart, dist, inc, phi, lambd, beta, psi
      Params dictionary keys format:
       [m1,               # Redshifted mass of body 1 (solar masses)
        m2,               # Redshifted mass of body 2 (solar masses)
        chi1,             # Dimensionless spin of body 1 (in [-1, 1])
        chi2,             # Dimensionless spin of body 2 (in [-1, 1])

        fstart,           # Frequency at the start of the observations (Hz)
        dist,             # Luminosity distance (Mpc)
        inc,              # Inclination angle (rad)
        phi,              # Observer's azimuthal phase (rad)
        lambda,           # Source longitude in SSB-frame (rad)
        beta,             # Source latitude in SSB-frame (rad)
        psi]              # Polarization angle (rad)

    Keyword args:
      minf                # Minimal frequency (Hz) (default 1e-5Hz)
      maxf                # Maximal frequency (Hz) (default 0.5Hz)
      fend                # Ending frequency for 22 mode (Hz) (None to ignore)
      tstart              # Starting time where f=fstart (yr) (default 0.)
      tmin                # Starting time of observation window (yr) (default None, tstart)
      tmax                # Ending time of observation window (yr) (default None, ignore)
      phiref              # Ref. phase (rad) at fref_for_phiref (default 0.)
      fref_for_phiref     # Ref. frequency for phase (Hz) (default None for fstart, set to 0 for fpeak)
                            NOTE: tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
                            NOTE: with tref=0, force_phiref_fref has no incidence
      toffset             # Extra time shift applied to the waveform (s)
      acc                 # Target interpolation accuracy for inspiral
      approximant         # Waveform approximant - for now has to be PhenomD
    """

    params = pytools.complete_mass_params(params)
    params = pytools.complete_spin_params(params)

    m1 = params['m1']
    m2 = params['m2']
    chi1 = params['chi1']
    chi2 = params['chi2']
    fstart = params['fstart']
    dist = params['dist']
    inc = params['inc']
    phi = params['phi']
    lambd = params['lambda']
    beta = params['beta']
    psi = params['psi']
    if params.get('Lframe', False):
        raise ValueError('Source params given in the L-frame, not implemented.')

    # Check args
    if not (approximant=='IMRPhenomD'):
        raise ValueError('Approximant %s not supported yet.' % (approximant))

    # Units
    q = m1 / m2
    eta = pytools.etaofq(q)
    M = m1 + m2
    Ms = M * pyconstants.MTSUN_SI
    Mfmin = Ms * minf
    Mfmax = Ms * maxf
    Mfmax_model = 0.2
    Mfmax = np.fmin(Mfmax, Mfmax_model)
    maxf = Mfmax / Ms

    # Deal with arguments
    fLow = np.fmax(fstart, minf)
    fHigh = maxf
    if fend is not None: fHigh = np.fmin(fHigh, fend)

    # Setting fref for time and phase
    # tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
    tref = 0.
    fref_for_tref = fstart
    # Specifying fref_for_phiref, phiref defines the source frame
    # If fref_for_phiref is None (default), use fstart
    if fref_for_phiref is None: fref_for_phiref = fstart
    # No Deltat allowed in principle, since we set the time at fstart
    Deltat = 0.

    # Take into account time cuts if specified
    # TODO: this repeats the initialization of the waveform, not optimal
    # NOTE: the cut in tmax is ignored if tmax > tpeak
    if (tmin is not None) or (tmax is not None):
        fstart_t_acc = 1e-6 # Hardcoded accuracy of f(t) function in s
        mock_gridfreq = np.array([fLow, fHigh])
        mock_wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(mock_gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=False, Deltat=Deltat)
        mock_tpeak = mock_wfClass.get_tpeak()
        if tmin is not None:
            tmin_s = tmin * pyconstants.YRSID_SI
            fLow_tmin = mock_wfClass.compute_foft(tmin_s, fLow, fstart_t_acc)
            fLow = np.fmax(fLow, fLow_tmin)
        if tmax is not None:
            tmax_s = tmax * pyconstants.YRSID_SI
            if tmax_s < mock_tpeak:
                fHigh_tmax = mock_wfClass.compute_foft(tmax_s, fHigh, fstart_t_acc)
                fHigh = np.fmin(fHigh, fHigh_tmax)

    # Combined frequency grid for the waveform
    gridfreqClass = pytools.FrequencyGrid(fLow, fHigh, M, q, acc=acc)
    gridfreq = gridfreqClass.get_freq()

    # Generate IMRPhenomD waveform
    wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=False, Deltat=Deltat)
    #FIXME  This has been adapted by John following Sylvain;s changes to waveform interface to support HM
    wfhlm = wfClass.get_waveform()
    freq = wfhlm[(2,2)]['freq']
    amp = wfhlm[(2,2)]['amp']
    phase = wfhlm[(2,2)]['phase']
    fpeak = wfClass.get_fpeak()

    # Apply an extra time offset (formerly tref, used for John's likelihood)
    phase += 2 * np.pi * freq * toffset

    # Build spline for tf
    phaseov2pisplineClass = pyspline.CubicSpline(freq, phase / (2*np.pi))
    tfspline = phaseov2pisplineClass.get_spline_d()
    tf = tfspline[:,1].copy()

    # tf=0 in the waveform corresponds to t0 in SSB time
    # We therefore fix t0 to tstart
    t0 = tstart

    # Inverse SPA to get TD signal, apply modulations/lensing
    # TODO: implement lensing and Shapiro time delay
    signal_fd = np.array([freq, amp, phase]).T
    signal_td = pytools.signal_inverse_spa(signal_fd, sign=1)

    # Output
    h22td = {}
    h22td['t'] = signal_td[:,0]
    h22td['amp'] = signal_td[:,1]
    h22td['phase'] = signal_td[:,2]

    return h22td

def GenerateLISATDI_SOBH_AGN(params, minf=1e-5, maxf=0.5, fend=None, tstart=0., tmin=None, tmax=None, fref_for_phiref=None, phiref=0., toffset=0., TDI='TDIAET', acc=1e-4, order_fresnel_stencil=0, approximant='IMRPhenomD', LISAconst=pyresponse.LISAconstProposal, responseapprox='full', frozenLISA=False, TDIrescaled=True, shapiro_delay=True, **kwargs):
    """m1, m2, chi1, chi2, fstart, dist, inc, phi, lambd, beta, psi
    Args:
      params              # Dictionary of input parameters for the signal
      Params dictionary keys format:
       [m1,               # Redshifted mass of body 1 (solar masses)
        m2,               # Redshifted mass of body 2 (solar masses)
        chi1,             # Dimensionless spin of body 1 (in [-1, 1])
        chi2,             # Dimensionless spin of body 2 (in [-1, 1])

        fstart,           # Frequency at the start of the observations (Hz)
        dist,             # Luminosity distance (Mpc)
        inc,              # Inclination angle (rad)
        phi,              # Observer's azimuthal phase (rad)
        lambda,           # Source longitude in SSB-frame (rad)
        beta,             # Source latitude in SSB-frame (rad)
        psi,              # Polarization angle (rad)
        Omega_AGN         # Orbital angular velocity around AGN (rad.s^-1)
        Rcostheta_AGN     # R/c*cos(theta) for orbit around AGN (s)
        Rsintheta_AGN     # R/c*sin(theta) for orbit around AGN (s)
        phi_AGN           # Initial phase of orbit around AGN (rad)

    Keyword args:
      minf                # Minimal frequency (Hz) (default 1e-5Hz)
      maxf                # Maximal frequency (Hz) (default 0.5Hz)
      fend                # Ending frequency for 22 mode (Hz) (None to ignore)
      tstart              # Starting time where f=fstart (yr) (default 0.)
      tmin                # Starting time of observation window (yr) (default None, tstart)
      tmax                # Ending time of observation window (yr) (default None, ignore)
      phiref              # Ref. phase (rad) at fref_for_phiref (default 0.)
      fref_for_phiref     # Ref. frequency for phase (Hz) (default None for fstart, set to 0 for fpeak)
                            NOTE: tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
                            NOTE: with tref=0, force_phiref_fref has no incidence
      toffset             # Extra time shift applied to the waveform (s)
      TDI                 # Choice of TDI channels (default TDIAET)
      acc                 # Target interpolation accuracy for inspiral
      order_fresnel_stencil # Order of Fresnel stencil in response
      approximant         # Waveform approximant
      LISAconst           # LISA constellation constants
      responseapprox      # Approximation in the response (full, lowfL, lowf)
      frozenLISA          # Freeze LISA motion
      TDIrescaled         # Rescale TDI variables (by prefactor common in TDI
                            and noise)
      shapiro_delay       # Include Shapiro time delay (default True)
    """

    params = pytools.complete_mass_params(params)
    params = pytools.complete_spin_params(params)

    # Output dict
    wftdi = {}
    wftdi['params'] = params.copy()

    m1 = params['m1']
    m2 = params['m2']
    chi1 = params['chi1']
    chi2 = params['chi2']
    fstart = params['fstart']
    dist = params['dist']
    inc = params['inc']
    phi = params['phi']
    lambd = params['lambda']
    beta = params['beta']
    psi = params['psi']
    Omega_AGN = params['Omega_AGN']
    Rcostheta_AGN = params['Rcostheta_AGN']
    Rsintheta_AGN = params['Rsintheta_AGN']
    phi_AGN = params['phi_AGN']
    if params.get('Lframe', False):
        raise ValueError('Source params given in the L-frame, not implemented.')

    # Convert AGN params to dictionary
    AGNparams = {}
    Rs_AGN = np.sqrt(Rcostheta_AGN**2 + Rsintheta_AGN**2)
    theta_AGN = np.arctan2(Rsintheta_AGN, Rcostheta_AGN)
    M_AGN = Rs_AGN**3 * Omega_AGN**2 / pyconstants.MTSUN_SI
    RM_AGN = Rs_AGN / (M_AGN * pyconstants.MTSUN_SI)
    AGNparams['M_AGN'] = M_AGN
    AGNparams['R_AGN'] = RM_AGN
    AGNparams['theta_AGN'] = theta_AGN
    AGNparams['phi_AGN'] = phi_AGN
    AGNparams['shapiro_delay'] = shapiro_delay

    # Check args
    if not (approximant=='IMRPhenomD'):
        raise ValueError('Approximant %s not supported yet.' % (approximant))
    if not TDI=='TDIAET':
        raise ValueError('Only TDI=TDIAET implemented for now.')
    if not order_fresnel_stencil==0:
        raise ValueError('Only order_fresnel_stencil=0 implemented for now.')

    # Units
    q = m1 / m2
    eta = pytools.etaofq(q)
    M = m1 + m2
    Ms = M * pyconstants.MTSUN_SI
    Mfmin = Ms * minf
    Mfmax = Ms * maxf
    Mfmax_model = 0.2
    Mfmax = np.fmin(Mfmax, Mfmax_model)
    maxf = Mfmax / Ms

    # Deal with arguments
    fLow = np.fmax(fstart, minf)
    fHigh = maxf
    if fend is not None: fHigh = np.fmin(fHigh, fend)

    # Setting fref for time and phase
    # tref, fref_for_tref are not free, we set tf=0 (tSSB=t0) at fstart
    tref = 0.
    fref_for_tref = fstart
    # Specifying fref_for_phiref, phiref defines the source frame
    # If fref_for_phiref is None (default), use fstart
    if fref_for_phiref is None: fref_for_phiref = fstart
    # No Deltat allowed in principle, since we set the time at fstart
    Deltat = 0.

    # Take into account time cuts if specified
    # TODO: this repeats the initialization of the waveform, not optimal
    # NOTE: the cut in tmax is ignored if tmax > tpeak
    if (tmin is not None) or (tmax is not None):
        fstart_t_acc = 1e-6 # Hardcoded accuracy of f(t) function in s
        mock_gridfreq = np.array([fLow, fHigh])
        mock_wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(mock_gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=False, Deltat=Deltat)
        mock_tpeak = mock_wfClass.get_tpeak()
        if tmin is not None:
            tmin_s = tmin * pyconstants.YRSID_SI
            fLow_tmin = mock_wfClass.compute_foft(tmin_s, fLow, fstart_t_acc)
            fLow = np.fmax(fLow, fLow_tmin)
        if tmax is not None:
            tmax_s = tmax * pyconstants.YRSID_SI
            if tmax_s < mock_tpeak:
                fHigh_tmax = mock_wfClass.compute_foft(tmax_s, fHigh, fstart_t_acc)
                fHigh = np.fmin(fHigh, fHigh_tmax)

    # Combined frequency grid for the waveform
    gridfreqClass = pytools.FrequencyGrid(fLow, fHigh, M, q, acc=acc)
    gridfreq = gridfreqClass.get_freq()

    # Generate IMRPhenomD waveform
    wfClass = pyIMRPhenomD.IMRPhenomDh22AmpPhase(gridfreq, m1, m2, chi1, chi2, dist, tref=tref, phiref=phiref, fref_for_tref=fref_for_tref, fref_for_phiref=fref_for_phiref, force_phiref_fref=False, Deltat=Deltat)
    #FIXME  This has been adapted by John following Sylvain;s changes to waveform interface to support HM
    wfhlm = wfClass.get_waveform()
    freq = wfhlm[(2,2)]['freq']
    amp = wfhlm[(2,2)]['amp']
    phase = wfhlm[(2,2)]['phase']
    fpeak = wfClass.get_fpeak()
    wftdi['fpeak'] = fpeak
    wftdi['modes'] = [(2,2)]

    # Apply an extra time offset (formerly tref, used for John's likelihood)
    phase += 2 * np.pi * freq * toffset

    # Build spline for tf
    phaseov2pisplineClass = pyspline.CubicSpline(freq, phase / (2*np.pi))
    tfspline = phaseov2pisplineClass.get_spline_d()
    tf = tfspline[:,1].copy()

    # tf=0 in the waveform corresponds to t0 in SSB time
    # We therefore fix t0 to tstart
    t0 = tstart

    # Inverse SPA to get TD signal, apply modulations/lensing
    # TODO: implement lensing and Shapiro time delay
    signal_fd = np.array([freq, amp, phase]).T
    signal_td = pytools.signal_inverse_spa(signal_fd, sign=1)
    signal_td_modulated = pyresponse.doppler_modulated_signal_td(signal_td, t0=t0, **AGNparams)
    wftdi['signal_td'] = signal_td
    wftdi['signal_td_modulated'] = signal_td_modulated

    # Divide signal in chirping, anti-chirping, quasi-monochromatic bands
    # TODO: for now fdot_exclude_mono is fixed arbitrarily
    t_bands = pyresponse.chirp_bands(signal_td_modulated[:,0], signal_td_modulated[:,2], fdot_exclude_mono=2e-12)
    signals_td = pyresponse.chirping_signal_decomp(signal_td_modulated, t_bands, window_fraction=0.)
    t_bands_chirp = []
    signals_td_chirp = []
    t_bands_mono = []
    signals_td_mono = []
    for i, band in enumerate(t_bands):
        if band[2]==0:
            t_bands_mono += [t_bands[i]]
            signals_td_mono += [signals_td[i]]
        else:
            t_bands_chirp += [t_bands[i]]
            signals_td_chirp += [signals_td[i]]
    # SPA of chirping or anti-chirping signal pieces
    signals_fd_chirp = []
    tf_bands = []
    for i,s in enumerate(signals_td_chirp):
        s_fd = pytools.signal_spa(s, sign=t_bands_chirp[i][2])
        phaseov2pisplineClass = pyspline.CubicSpline(s_fd[:,0], s_fd[:,2] / (2*np.pi))
        tfspline = phaseov2pisplineClass.get_spline_d()
        tf = tfspline[:,1].copy()
        signals_fd_chirp += [s_fd]
        tf_bands += [tf]

    # Response and output, piece by piece for the decomposed signal
    l = 2
    m = 2
    wftdi[(2,2)] = {}
    wftdi[(2,2)]['wftdi_fd_bands'] = []
    for i, signal_fd in enumerate(signals_fd_chirp):
        tf = tf_bands[i]
        tdiClass = pyresponse.LISAFDresponseTDI3Chan(signal_fd[:,0], tf, t0, l, m, inc, phi, lambd, beta, psi, TDI='TDIAET', LISAconst=LISAconst, responseapprox=responseapprox, frozenLISA=frozenLISA, TDIrescaled=TDIrescaled)
        phaseRdelay, transferL1, transferL2, transferL3 = tdiClass.get_response()
        # Output
        wftdi_band = {}
        wftdi_band['freq'] = signal_fd[:,0]
        wftdi_band['amp'] = signal_fd[:,1]
        wftdi_band['phase'] = signal_fd[:,2]
        wftdi_band['phaseRdelay'] = phaseRdelay
        wftdi_band['transferL1'] = transferL1
        wftdi_band['transferL2'] = transferL2
        wftdi_band['transferL3'] = transferL3
        wftdi_band['tf'] = tf.copy()
        wftdi[(2,2)]['wftdi_fd_bands'] += [wftdi_band]

    return wftdi

def GetCAmpPhaseTDI(wftdi):

    modes = wftdi['modes']

    tdi = {}
    tdi['modes'] = list(modes)

    # Loop over modes
    for lm in modes:

        freq = wftdi[lm]['freq']

        # Complex amplitudes, not noise-weighted
        if len(wftdi[lm]['amp'])!=len(wftdi[lm]['transferL1']):
            print("WARNING:mismatched lengths: len(wftdi[lm]['amp'])!=len(wftdi[lm]['transferL1']): ",len(wftdi[lm]['amp']),"!=",len(wftdi[lm]['transferL1'])," lm=",lm)
        camp1 = wftdi[lm]['amp'] * wftdi[lm]['transferL1']
        camp2 = wftdi[lm]['amp'] * wftdi[lm]['transferL2']
        camp3 = wftdi[lm]['amp'] * wftdi[lm]['transferL3']
        # Total phase
        phasetot = wftdi[lm]['phase'] + wftdi[lm]['phaseRdelay']

        # Output
        tdi[lm] = {}
        tdi[lm]['freq'] = np.copy(freq)
        tdi[lm]['phase'] = np.copy(phasetot)
        tdi[lm]['amp_real_chan1'] = np.copy(np.real(camp1))
        tdi[lm]['amp_imag_chan1'] = np.copy(np.imag(camp1))
        tdi[lm]['amp_real_chan2'] = np.copy(np.real(camp2))
        tdi[lm]['amp_imag_chan2'] = np.copy(np.imag(camp2))
        tdi[lm]['amp_real_chan3'] = np.copy(np.real(camp3))
        tdi[lm]['amp_imag_chan3'] = np.copy(np.imag(camp3))
    return tdi


def GetInterpolatedNoiseweightedCAmpPhaseTDI(wftdi,
                                    LISAnoise=pyLISAnoise.LISAnoiseSciRDv1,
                                    TDI='TDIAET', TDIrescaled=True,
                                    LISAconst=pyresponse.LISAconstProposal):

    modes = wftdi['modes']

    weightedtdi = {}
    weightedtdi['modes'] = list(modes)
    weightedtdi['LISAnoise'] = LISAnoise


    noise_evaluator = pyLISAnoise.initialize_noise(LISAnoise,
                                        TDI=TDI, TDIrescaled=TDIrescaled,
                                        LISAconst=LISAconst)

    # Loop over modes
    for lm in modes:
        freq = wftdi[lm]['freq']

        Sn1_vals, Sn2_vals, Sn3_vals = pyLISAnoise.evaluate_noise(
                          LISAnoise, noise_evaluator, freq,
                          TDI=TDI, TDIrescaled=TDIrescaled, LISAconst=LISAconst)

        # Noise-weighted complex amplitudes
        camp1 = 1./Sn1_vals * wftdi[lm]['amp'] * wftdi[lm]['transferL1']
        camp2 = 1./Sn2_vals * wftdi[lm]['amp'] * wftdi[lm]['transferL2']
        camp3 = 1./Sn3_vals * wftdi[lm]['amp'] * wftdi[lm]['transferL3']
        # Total phase
        phasetot = wftdi[lm]['phase'] + wftdi[lm]['phaseRdelay']

        # We have to pass in contiguous C arrays for C functions
        # np.real/np.imag return a numpy view, so a np.copy is necessary
        camp1real = np.copy(np.real(camp1))
        camp1imag = np.copy(np.imag(camp1))
        camp2real = np.copy(np.real(camp2))
        camp2imag = np.copy(np.imag(camp2))
        camp3real = np.copy(np.real(camp3))
        camp3imag = np.copy(np.imag(camp3))

        # Spline interpolation
        spline_phasetotClass = pyspline.CubicSpline(freq, phasetot)
        spline_camp1realClass = pyspline.CubicSpline(freq, camp1real)
        spline_camp1imagClass = pyspline.CubicSpline(freq, camp1imag)
        spline_camp2realClass = pyspline.CubicSpline(freq, camp2real)
        spline_camp2imagClass = pyspline.CubicSpline(freq, camp2imag)
        spline_camp3realClass = pyspline.CubicSpline(freq, camp3real)
        spline_camp3imagClass = pyspline.CubicSpline(freq, camp3imag)

        # Output
        weightedtdi[lm] = {}
        weightedtdi[lm]['spline_phase'] = spline_phasetotClass.get_spline()
        weightedtdi[lm]['spline_amp_real_chan1'] = spline_camp1realClass.get_spline()
        weightedtdi[lm]['spline_amp_imag_chan1'] = spline_camp1imagClass.get_spline()
        weightedtdi[lm]['spline_amp_real_chan2'] = spline_camp2realClass.get_spline()
        weightedtdi[lm]['spline_amp_imag_chan2'] = spline_camp2imagClass.get_spline()
        weightedtdi[lm]['spline_amp_real_chan3'] = spline_camp3realClass.get_spline()
        weightedtdi[lm]['spline_amp_imag_chan3'] = spline_camp3imagClass.get_spline()

    return weightedtdi

def GetCAmpPhaseTDIbands(wftdibands):

    modes = wftdibands['modes']

    tdibands = {}
    tdibands['modes'] = list(modes)

    # Loop over modes
    for lm in modes:
        tdibandslist = []

        for wftdilmband in wftdibands[lm]['wftdi_fd_bands']:

            freq = wftdilmband['freq']

            # Complex amplitudes, not noise-weighted
            camp1 = wftdilmband['amp'] * wftdilmband['transferL1']
            camp2 = wftdilmband['amp'] * wftdilmband['transferL2']
            camp3 = wftdilmband['amp'] * wftdilmband['transferL3']
            # Total phase
            phasetot = wftdilmband['phase'] + wftdilmband['phaseRdelay']

            # Output
            tdilmband = {}
            tdilmband['freq'] = np.copy(freq)
            tdilmband['phase'] = np.copy(phasetot)
            tdilmband['amp_real_chan1'] = np.copy(np.real(camp1))
            tdilmband['amp_imag_chan1'] = np.copy(np.imag(camp1))
            tdilmband['amp_real_chan2'] = np.copy(np.real(camp2))
            tdilmband['amp_imag_chan2'] = np.copy(np.imag(camp2))
            tdilmband['amp_real_chan3'] = np.copy(np.real(camp3))
            tdilmband['amp_imag_chan3'] = np.copy(np.imag(camp3))

            tdibandslist += [tdilmband]

        tdibands[lm] = {'tdi_fd_bands': tdibandslist}

    return tdibands


def GetInterpolatedNoiseweightedCAmpPhaseTDIbands(wftdibands,
                                    LISAnoise=pyLISAnoise.LISAnoiseSciRDv1,
                                    TDI='TDIAET', TDIrescaled=True,
                                    LISAconst=pyresponse.LISAconstProposal):

    modes = wftdibands['modes']

    weightedtdibands = {}
    weightedtdibands['modes'] = list(modes)
    weightedtdibands['LISAnoise'] = LISAnoise


    noise_evaluator = pyLISAnoise.initialize_noise(LISAnoise,
                                        TDI=TDI, TDIrescaled=TDIrescaled,
                                        LISAconst=LISAconst)

    # Loop over modes
    for lm in modes:
        weightedtdibandslist = []

        for wftdilmband in wftdibands[lm]['wftdi_fd_bands']:

            freq = wftdilmband['freq']

            Sn1_vals, Sn2_vals, Sn3_vals = pyLISAnoise.evaluate_noise(
                          LISAnoise, noise_evaluator, freq,
                          TDI=TDI, TDIrescaled=TDIrescaled, LISAconst=LISAconst)

            # Noise-weighted complex amplitudes
            camp1 = 1./Sn1_vals * wftdilmband['amp'] * wftdilmband['transferL1']
            camp2 = 1./Sn2_vals * wftdilmband['amp'] * wftdilmband['transferL2']
            camp3 = 1./Sn3_vals * wftdilmband['amp'] * wftdilmband['transferL3']
            # Total phase
            phasetot = wftdilmband['phase'] + wftdilmband['phaseRdelay']

            # We have to pass in contiguous C arrays for C functions
            # np.real/np.imag return a numpy view, so a np.copy is necessary
            camp1real = np.copy(np.real(camp1))
            camp1imag = np.copy(np.imag(camp1))
            camp2real = np.copy(np.real(camp2))
            camp2imag = np.copy(np.imag(camp2))
            camp3real = np.copy(np.real(camp3))
            camp3imag = np.copy(np.imag(camp3))

            # Spline interpolation
            spline_phasetotClass = pyspline.CubicSpline(freq, phasetot)
            spline_camp1realClass = pyspline.CubicSpline(freq, camp1real)
            spline_camp1imagClass = pyspline.CubicSpline(freq, camp1imag)
            spline_camp2realClass = pyspline.CubicSpline(freq, camp2real)
            spline_camp2imagClass = pyspline.CubicSpline(freq, camp2imag)
            spline_camp3realClass = pyspline.CubicSpline(freq, camp3real)
            spline_camp3imagClass = pyspline.CubicSpline(freq, camp3imag)

            # Output
            weightedtdilmband = {}
            weightedtdilmband['spline_phase'] = spline_phasetotClass.get_spline()
            weightedtdilmband['spline_amp_real_chan1'] = spline_camp1realClass.get_spline()
            weightedtdilmband['spline_amp_imag_chan1'] = spline_camp1imagClass.get_spline()
            weightedtdilmband['spline_amp_real_chan2'] = spline_camp2realClass.get_spline()
            weightedtdilmband['spline_amp_imag_chan2'] = spline_camp2imagClass.get_spline()
            weightedtdilmband['spline_amp_real_chan3'] = spline_camp3realClass.get_spline()
            weightedtdilmband['spline_amp_imag_chan3'] = spline_camp3imagClass.get_spline()

            weightedtdibandslist += [weightedtdilmband]

        weightedtdibands[lm] = {'weightedtdi_fd_bands': weightedtdibandslist}

    return weightedtdibands

def ComputeCumulativeSNRTDI(wftdi,
                        LISAnoise=pyLISAnoise.LISAnoiseSciRDv1,
                        TDI='TDIAET', TDIrescaled=True,
                        LISAconst=pyresponse.LISAconstProposal):

    modes = wftdi['modes']
    if not modes==[(2,2)]:
        raise ValueError('This function only supports (2,2) mode for now.')

    freq = wftdi[(2,2)]['freq']
    tf = wftdi[(2,2)]['tf']

    noise_evaluator = pyLISAnoise.initialize_noise(LISAnoise,
                                        TDI=TDI, TDIrescaled=TDIrescaled,
                                        LISAconst=LISAconst)
    Sn1_vals, Sn2_vals, Sn3_vals = pyLISAnoise.evaluate_noise(
                      LISAnoise, noise_evaluator, freq,
                      TDI=TDI, TDIrescaled=TDIrescaled, LISAconst=LISAconst)

    lm = (2,2)
    int1 = 4./Sn1_vals * np.abs(wftdi[lm]['amp'] * wftdi[lm]['transferL1'])**2
    int2 = 4./Sn2_vals * np.abs(wftdi[lm]['amp'] * wftdi[lm]['transferL2'])**2
    int3 = 4./Sn3_vals * np.abs(wftdi[lm]['amp'] * wftdi[lm]['transferL3'])**2

    splineClass_int1 = pyspline.CubicSpline(freq, int1)
    splineClass_int2 = pyspline.CubicSpline(freq, int2)
    splineClass_int3 = pyspline.CubicSpline(freq, int3)

    cumulsnr2_chan1 = splineClass_int1.get_spline_int()[:,1]
    cumulsnr2_chan2 = splineClass_int2.get_spline_int()[:,1]
    cumulsnr2_chan3 = splineClass_int3.get_spline_int()[:,1]
    cumulsnr2 = cumulsnr2_chan1 + cumulsnr2_chan2 + cumulsnr2_chan3

    cumulsnr = {}
    cumulsnr['freq'] = freq.copy()
    cumulsnr['tf'] = tf.copy()
    cumulsnr['SNRcumul'] = np.sqrt(cumulsnr2)
    cumulsnr['SNR1cumul'] = np.sqrt(cumulsnr2_chan1)
    cumulsnr['SNR2cumul'] = np.sqrt(cumulsnr2_chan2)
    cumulsnr['SNR3cumul'] = np.sqrt(cumulsnr2_chan3)
    return cumulsnr

def CumulSNRLISATDI_SMBH(params, **kwargs):

    # Need direct access to these keyword args
    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
    TDI = kwargs.get('TDI', 'TDIAET')
    TDIrescaled = kwargs.get('TDIrescaled', True)
    LISAconst = kwargs.get('LISAconst', pyresponse.LISAconstProposal)
    # Those will be ignored
    overlap_method = kwargs.pop('overlap_method', 'fresnel')
    overlap_deltaf = kwargs.pop('overlap_deltaf', 1./(60.*86400.))

    wftdi = GenerateLISATDI_SMBH(params, **kwargs)

    cumulsnr = ComputeCumulativeSNRTDI(wftdi,
                                   LISAnoise=LISAnoise, TDI=TDI,
                                   TDIrescaled=TDIrescaled, LISAconst=LISAconst)
    return cumulsnr

def CumulSNRLISATDI_SOBH(params, **kwargs):

    # Need direct access to these keyword args
    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
    TDI = kwargs.get('TDI', 'TDIAET')
    TDIrescaled = kwargs.get('TDIrescaled', True)
    LISAconst = kwargs.get('LISAconst', pyresponse.LISAconstProposal)

    wftdi = GenerateLISATDI_SOBH(params, **kwargs)

    cumulsnr = ComputeCumulativeSNRTDI(wftdi,
                                   LISAnoise=LISAnoise, TDI=TDI,
                                   TDIrescaled=TDIrescaled, LISAconst=LISAconst)
    return cumulsnr

def EvaluateTDIFreqseries(tdi, freqs):

    # Sets of modes
    modes = tdi['modes']

    tdifreqseries = {}
    tdifreqseries['modes'] = modes

    # Loop over modes to evaluate values for wf2, which is already interpolated
    for lm in modes:
        # Build interpolating splines
        spline_amp_real_chan1Class = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['amp_real_chan1'])
        spline_amp_imag_chan1Class = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['amp_imag_chan1'])
        spline_amp_real_chan2Class = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['amp_real_chan2'])
        spline_amp_imag_chan2Class = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['amp_imag_chan2'])
        spline_amp_real_chan3Class = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['amp_real_chan3'])
        spline_amp_imag_chan3Class = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['amp_imag_chan3'])
        spline_phaseClass = pyspline.CubicSpline(tdi[lm]['freq'], tdi[lm]['phase'])
        spline_amp_real_chan1 = spline_amp_real_chan1Class.get_spline()
        spline_amp_imag_chan1 = spline_amp_imag_chan1Class.get_spline()
        spline_amp_real_chan2 = spline_amp_real_chan2Class.get_spline()
        spline_amp_imag_chan2 = spline_amp_imag_chan2Class.get_spline()
        spline_amp_real_chan3 = spline_amp_real_chan3Class.get_spline()
        spline_amp_imag_chan3 = spline_amp_imag_chan3Class.get_spline()
        spline_phase = spline_phaseClass.get_spline()
        # Evaluate splines
        ampreal_chan1 = pyspline.spline_eval_vector(spline_amp_real_chan1, freqs, extrapol_zero=True)
        ampimag_chan1 = pyspline.spline_eval_vector(spline_amp_imag_chan1, freqs, extrapol_zero=True)
        ampreal_chan2 = pyspline.spline_eval_vector(spline_amp_real_chan2, freqs, extrapol_zero=True)
        ampimag_chan2 = pyspline.spline_eval_vector(spline_amp_imag_chan2, freqs, extrapol_zero=True)
        ampreal_chan3 = pyspline.spline_eval_vector(spline_amp_real_chan3, freqs, extrapol_zero=True)
        ampimag_chan3 = pyspline.spline_eval_vector(spline_amp_imag_chan3, freqs, extrapol_zero=True)
        phase = pyspline.spline_eval_vector(spline_phase, freqs, extrapol_zero=True)
        # Get complex values for the TDI freqseries
        eiphase = np.exp(1j*phase)
        tdi_chan1_vals = (ampreal_chan1 + 1j*ampimag_chan1) * eiphase
        tdi_chan2_vals = (ampreal_chan2 + 1j*ampimag_chan2) * eiphase
        tdi_chan3_vals = (ampreal_chan3 + 1j*ampimag_chan3) * eiphase
        # Output
        tdifreqseries[lm] = {}
        tdifreqseries[lm]['chan1'] = tdi_chan1_vals
        tdifreqseries[lm]['chan2'] = tdi_chan2_vals
        tdifreqseries[lm]['chan3'] = tdi_chan3_vals

    return tdifreqseries

def GenerateLISATDIFreqseries_SMBH(params, freqs,
                                   **kwargs):

    wftdi = GenerateLISATDI_SMBH(params, **kwargs)

    tdi = GetCAmpPhaseTDI(wftdi)

    tdifreqseries = EvaluateTDIFreqseries(tdi, freqs)

    return tdifreqseries

def GenerateLISATDIFreqseries_SOBH(params, freqs,
                                   **kwargs):

    wftdi = GenerateLISATDI_SOBH(params, **kwargs)

    tdi = GetCAmpPhaseTDI(wftdi)

    tdifreqseries = EvaluateTDIFreqseries(tdi, freqs)

    return tdifreqseries

def GenerateLISATDISignal_SMBH(params,
                               **kwargs):

    # Need direct access to these keyword args
    LISAnoise = kwargs.get('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
    TDI = kwargs.get('TDI', 'TDIAET')
    TDIrescaled = kwargs.get('TDIrescaled', True)
    LISAconst = kwargs.get('LISAconst', pyresponse.LISAconstProposal)
    overlap_method = kwargs.get('overlap_method', 'fresnel')
    # Only used for brute overlaps
    # TODO: have a better default, here roughly Nyquist for one month
    overlap_deltaf = kwargs.get('overlap_deltaf', 1./(60.*86400.))
    # Further options for brute-force overlaps
    force_include_boundaries = kwargs.pop('force_include_boundaries', True)
    brute_type = kwargs.pop('brute_type', 'right')

    wftdi = GenerateLISATDI_SMBH(params, **kwargs)

    tdi = GetCAmpPhaseTDI(wftdi)

    weightedtdi = GetInterpolatedNoiseweightedCAmpPhaseTDI(wftdi,
                                   LISAnoise=LISAnoise, TDI=TDI,
                                   TDIrescaled=TDIrescaled, LISAconst=LISAconst)

    # Computing SNR
    # Save mode-by-mode contributions to SNR^2 when using fresnel overlaps
    SNR2 = 0.
    SNR2modematrix = {}
    if overlap_method=='fresnel':
        nmodes = len(wftdi['modes'])
        SNR2modebymode = np.zeros((nmodes, nmodes), dtype=float)
        for i in range(nmodes):
            lm = wftdi['modes'][i]
            # # Mind the order of these two loops for j>=i and j<i
            # for j in range(i, nmodes):
            #     lpmp = wftdi['modes'][j]
            #     SNR2modebymode[i,j] = pyoverlap.fresnel_overlap_3chan(tdi[lm],
            #                                                   weightedtdi[lpmp])
            # for j in range(i):
            #     lpmp = wftdi['modes'][j]
            #     SNR2modebymode[i,j] = SNR2modebymode[j,i]
            # Here we do NOT exploit the symmetrization, keeping the errors
            # Will artificially eliminate Fresnel errors for injection
            for j in range(nmodes):
                lpmp = wftdi['modes'][j]
                SNR2modebymode[i,j] = pyoverlap.fresnel_overlap_3chan(tdi[lm],
                                                              weightedtdi[lpmp])
        SNR2modematrix['modes'] = copy.deepcopy(wftdi['modes'])
        SNR2modematrix['matrix'] = SNR2modebymode
        SNR2 = np.sum(SNR2modebymode)
    elif overlap_method=='brute':
        SNR2 = pyoverlap.brute_overlap_3chan(tdi, weightedtdi, overlap_deltaf,
                                             force_include_boundaries=force_include_boundaries,
                                             brute_type=brute_type)
    else:
        raise ValueError('overlap_method %s not recognized.' % overlap_method)

    tdisignal = {}
    tdisignal['SNR'] = np.sqrt(SNR2)
    tdisignal['SNR2modematrix'] = SNR2modematrix
    tdisignal['wftdi'] = wftdi
    tdisignal['tdi'] = tdi
    tdisignal['weightedtdi'] = weightedtdi

    return tdisignal

def GenerateLISATDISignal_SOBH(params,
                               **kwargs):

    # Need direct access to these keyword args
    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
    #if not 'lowf_add_pm_noise_f0' in LISAnoise:
    #    LISAnoise['lowf_add_pm_noise_f0']=0
    #    LISAnoise['lowf_add_pm_noise_alpha']=2
    TDI = kwargs.get('TDI', 'TDIAET')
    TDIrescaled = kwargs.get('TDIrescaled', True)
    LISAconst = kwargs.get('LISAconst', pyresponse.LISAconstProposal)

    wftdi = GenerateLISATDI_SOBH(params, **kwargs)

    tdi = GetCAmpPhaseTDI(wftdi)

    weightedtdi = GetInterpolatedNoiseweightedCAmpPhaseTDI(wftdi,
                                   LISAnoise=LISAnoise, TDI=TDI,
                                   TDIrescaled=TDIrescaled, LISAconst=LISAconst)

    #SNR2 = pyoverlap.fresnel_overlap_3chan(tdi, weightedtdi)
    SNR2 = 0
    for lm in wftdi['modes']:
        for lpmp in wftdi['modes']:
            SNR2 += pyoverlap.fresnel_overlap_3chan(tdi[lm], weightedtdi[lpmp])

    tdisignal = {}
    tdisignal['SNR'] = np.sqrt(SNR2)
    tdisignal['wftdi'] = wftdi
    tdisignal['tdi'] = tdi
    tdisignal['weightedtdi'] = weightedtdi

    return tdisignal

def GenerateLISATDISignal_SOBH_AGN(params,
                                   **kwargs):

    # Need direct access to these keyword args
    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
    #if not 'lowf_add_pm_noise_f0' in LISAnoise:
    #    LISAnoise['lowf_add_pm_noise_f0']=0
    #    LISAnoise['lowf_add_pm_noise_alpha']=2
    TDI = kwargs.get('TDI', 'TDIAET')
    TDIrescaled = kwargs.get('TDIrescaled', True)
    LISAconst = kwargs.get('LISAconst', pyresponse.LISAconstProposal)

    wftdibands = GenerateLISATDI_SOBH_AGN(params, **kwargs)

    tdibands = GetCAmpPhaseTDIbands(wftdibands)

    weightedtdibands = GetInterpolatedNoiseweightedCAmpPhaseTDIbands(wftdibands,
                                   LISAnoise=LISAnoise, TDI=TDI,
                                   TDIrescaled=TDIrescaled, LISAconst=LISAconst)

    #SNR2 = pyoverlap.fresnel_overlap_3chan(tdi, weightedtdi)
    SNR2 = 0
    for lm in wftdibands['modes']:
        for lpmp in wftdibands['modes']:
            SNR2 += pyoverlap.fresnel_overlap_3chan_bands(
                                 tdibands[lm]['tdi_fd_bands'],
                                 weightedtdibands[lpmp]['weightedtdi_fd_bands'])

    tdisignal = {}
    tdisignal['SNR'] = np.sqrt(SNR2)
    tdisignal['wftdi_bands'] = wftdibands
    tdisignal['tdi_bands'] = tdibands
    tdisignal['weightedtdi_bands'] = weightedtdibands

    return tdisignal

def LogLikelihoodLISA_SMBH(params_tem, tdisignal_inj,
                           **kwargs):

    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
    overlap_method = kwargs.pop('overlap_method', 'fresnel')
    # Only used for brute overlaps
    # TODO: have a better default, here roughly Nyquist for one month
    overlap_deltaf = kwargs.pop('overlap_deltaf', 1./(60.*86400.))
    # Further options for brute-force overlaps
    force_include_boundaries = kwargs.pop('force_include_boundaries', True)
    brute_type = kwargs.pop('brute_type', 'right')

    tdisignal_tem = GenerateLISATDISignal_SMBH(params_tem,
                                               LISAnoise=LISAnoise,
                                               overlap_method=overlap_method,
                                               overlap_deltaf=overlap_deltaf,
                                               **kwargs)

    # Double sum over all mode contributions
    modes_inj = tdisignal_inj['wftdi']['modes']
    modes_tem = tdisignal_tem['wftdi']['modes']
    overlap_inj_tem = 0.
    if overlap_method=='fresnel':
        for lm in modes_inj:
            for lpmp in modes_tem:
                overlap_inj_tem += pyoverlap.fresnel_overlap_3chan(
                                               tdisignal_tem['tdi'][lpmp],
                                               tdisignal_inj['weightedtdi'][lm])
    elif overlap_method=='brute':
        overlap_inj_tem = pyoverlap.brute_overlap_3chan(
                                        tdisignal_tem['tdi'],
                                        tdisignal_inj['weightedtdi'],
                                        overlap_deltaf,
                                        force_include_boundaries=force_include_boundaries,
                                        brute_type=brute_type)
    else:
        raise ValueError('overlap_method %s not recognized.' % overlap_method)

    SNR2_inj = tdisignal_inj['SNR']**2
    SNR2_tem = tdisignal_tem['SNR']**2
    lnLikelihood = overlap_inj_tem - 1./2 * SNR2_tem - 1./2 * SNR2_inj

    return lnLikelihood

def LogLikelihoodLISA_SOBH(params_tem, tdisignal_inj,
                           **kwargs):

    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)

    # # Check the LISA noise used
    # if (tdisignal_inj['weightedtdi']['LISAnoise'] != LISAnoise):
    #     raise ValueError('Using different LISAnoise in injection and template.')

    tdisignal_tem = GenerateLISATDISignal_SOBH(params_tem,
                                               LISAnoise=LISAnoise,
                                               **kwargs)

    #overlap_inj_tem = pyoverlap.fresnel_overlap_3chan(tdisignal_tem['tdi'],
    #                                               tdisignal_inj['weightedtdi'])
    # Double sum over all mode contributions
    modes_inj = tdisignal_inj['wftdi']['modes']
    modes_tem = tdisignal_tem['wftdi']['modes']
    overlap_inj_tem = 0
    for lm in modes_inj:
        for lpmp in modes_tem:
            overlap_inj_tem += pyoverlap.fresnel_overlap_3chan(
                tdisignal_tem['tdi'][lpmp],
                tdisignal_inj['weightedtdi'][lm])

    SNR2_inj = tdisignal_inj['SNR']**2
    SNR2_tem = tdisignal_tem['SNR']**2
    lnLikelihood = overlap_inj_tem - 1./2 * SNR2_tem - 1./2 * SNR2_inj

    return lnLikelihood

def LogLikelihoodLISA_SOBH_AGN(params_tem, tdisignal_bands_inj,
                               **kwargs):

    LISAnoise = kwargs.pop('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)

    # # Check the LISA noise used
    # if (tdisignal_inj['weightedtdi']['LISAnoise'] != LISAnoise):
    #     raise ValueError('Using different LISAnoise in injection and template.')

    tdisignal_bands_tem = GenerateLISATDISignal_SOBH_AGN(params_tem,
                                                        LISAnoise=LISAnoise,
                                                        **kwargs)

    #overlap_inj_tem = ≈pyoverlap.fresnel_overlap_3chan(tdisignal_tem['tdi'],
    #                                               tdisignal_inj['weightedtdi'])
    # Double sum over all mode contributions
    modes_inj = tdisignal_bands_inj['wftdi_bands']['modes']
    modes_tem = tdisignal_bands_tem['wftdi_bands']['modes']
    overlap_inj_tem = 0
    for lm in modes_inj:
        for lpmp in modes_tem:
            overlap_inj_tem += pyoverlap.fresnel_overlap_3chan_bands(
                tdisignal_bands_tem['tdi_bands'][lpmp]['tdi_fd_bands'],
                tdisignal_bands_inj['weightedtdi_bands'][lm]['weightedtdi_fd_bands'])

    SNR2_inj = tdisignal_bands_inj['SNR']**2
    SNR2_tem = tdisignal_bands_tem['SNR']**2
    lnLikelihood = overlap_inj_tem - 1./2 * SNR2_tem - 1./2 * SNR2_inj

    return lnLikelihood

class SimpleLikelihoodLISASMBH:
    """Simple likelihood class for LISA SMBH, single-source.
       "Simple" likelihood means varying only extrinsic angular params and dist
       in the low-f approximation for the response.
       The likelihood is then simply some pattern functions and Ylms multiplied
       by constant prefactors that are computed once and for all.
       See Eq. (68) in arXiv:2003.00357
    """

    def __init__(self, source_params, **waveform_params):
        """Constructor
        Args:
          source_params         # Dictionary of the source parameters
        Keyword args:
          waveform_params       # Waveform options and extra parameters
        """

        self.waveform_params = copy.deepcopy(waveform_params_smbh_default)
        self.waveform_params.update(waveform_params)
        # Dictionary update is shallow... we have to re-deepcopy
        self.waveform_params = copy.deepcopy(self.waveform_params)

        # If source params are given in the Lframe, convert to SSB
        if source_params.get('Lframe', False):
            self.source_params_SSBframe = lisatools.convert_Lframe_to_SSBframe(
                                source_params,
                                t0=self.waveform_params['t0'],
                                frozenLISA=self.waveform_params['frozenLISA'])
            self.source_params_Lframe = source_params.copy()
        else:
            self.source_params_SSBframe = source_params.copy()
            self.source_params_Lframe = lisatools.convert_SSBframe_to_Lframe(
                                source_params,
                                t0=self.waveform_params['t0'],
                                frozenLISA=self.waveform_params['frozenLISA'])

        # Necessary back-and-forth between strings and C enums
        # Convert in-place LISA constellation from string to dict
        # LISAconst = pyresponse.LISAconstDict[self.waveform_params['LISAconst']]
        # self.waveform_params['LISAconst'] = LISAconst
        # Convert in-place LISA instrumental noise from string to internal enum
        # LISAnoise = self.waveform_params['LISAnoise']
        # LISAnoise['InstrumentalNoise'] = pyLISAnoise.LISAnoiseDict[
        #                                         LISAnoise['InstrumentalNoise']]

        # Generate injection signal -- takes SSB frame params as input
        self.tdisignal_inj = GenerateLISATDISignal_SMBH(
                                                    self.source_params_SSBframe,
                                                    **self.waveform_params)


        # Compute <lm|l'm'> inner products
        # We follow the steps in GenerateLISATDISignal_SMBH, but need an extra
        # f-dependent weighting in overlaps
        # TODO: avoid duplication ?
        wftdi = self.tdisignal_inj['wftdi']
        modes = wftdi['modes']
        self.modes = modes

        # Get direct access to keyword arguments
        LISAconst = self.waveform_params.get('LISAconst', pyresponse.LISAconstProposal)
        if isinstance(LISAconst, str):
            LISAconst = pyresponse.LISAconstDict[LISAconst]
        L = LISAconst['OrbitL']
        LISAnoise = self.waveform_params.get('LISAnoise', pyLISAnoise.LISAnoiseSciRDv1)
        TDI = self.waveform_params.get('TDI', 'TDIAET')
        TDIrescaled = self.waveform_params.get('TDIrescaled', True)

        # Build a signal (6 pi f L) * hlm, see Eq. (67) in arXiv:2003.00357
        # We arbitrarily put the (6 pi f L) factor as a transfer function in A
        wftdi_scaled = {}
        wftdi_scaled['params'] = wftdi['params'].copy()
        wftdi_scaled['modes'] = wftdi['modes']
        # if (approximant=='IMRPhenomD'):
        #     wftdi_scaled['fpeak'] = wftdi['fpeak']
        for lm in modes:
            wftdi_scaled[lm] = {}
            wftdi_scaled[lm]['freq'] = wftdi[lm]['freq'].copy()
            wftdi_scaled[lm]['amp'] = wftdi[lm]['amp'].copy()
            wftdi_scaled[lm]['phase'] = wftdi[lm]['phase'].copy()
            wftdi_scaled[lm]['phaseRdelay'] = np.zeros_like(wftdi[lm]['freq'])
            wftdi_scaled[lm]['tf'] = wftdi[lm]['tf'].copy()

            # We will use 3-channels
            scaling = 6*np.pi * wftdi[lm]['freq'] * L/pyconstants.C_SI
            wftdi_scaled[lm]['transferL1'] = scaling
            wftdi_scaled[lm]['transferL2'] = np.zeros_like(wftdi[lm]['freq'])
            wftdi_scaled[lm]['transferL3'] = np.zeros_like(wftdi[lm]['freq'])

        tdi_scaled = GetCAmpPhaseTDI(wftdi_scaled)

        weightedtdi_scaled = GetInterpolatedNoiseweightedCAmpPhaseTDI(wftdi_scaled,
                                       LISAnoise=LISAnoise, TDI=TDI,
                                       TDIrescaled=TDIrescaled, LISAconst=LISAconst)

        # <lm|l'm'> scaled inner products, Eq. (67) in arXiv:2003.00357
        self.scaled_innerproduct = {}
        nmodes = len(modes)
        scaled_innerproduct_lm_lpmp = np.zeros((nmodes, nmodes), dtype=np.complex64)
        for i in range(nmodes):
            lm = modes[i]
            # Mind the order of these two loops for j>=i and j<i
            # for j in range(i, nmodes):
            for j in range(nmodes):
                lpmp = modes[j]
                # Note that the inner products used here are complex, no Re()
                scaled_innerproduct_lm_lpmp[i,j] = pyoverlap.fresnel_overlap_3chan(tdi_scaled[lm],
                                                              weightedtdi_scaled[lpmp], real=False)
            # for j in range(i):
            #     lpmp = modes[j]
            #     scaled_innerproduct_lm_lpmp[i,j] = np.conj(scaled_innerproduct_lm_lpmp[j,i])
        self.scaled_innerproduct['modes'] = copy.deepcopy(modes)
        self.scaled_innerproduct['matrix'] = scaled_innerproduct_lm_lpmp

        # sa_lm, se_lm for the injection
        self.dist_inj = self.source_params_Lframe['dist']
        self.sa_lm_inj, self.se_lm_inj = lisatools.sae_lm(self.source_params_Lframe, self.dist_inj, modes=modes)

    def lnL(self, template_params, **waveform_params_template):
        waveform_params_temp = copy.deepcopy(self.waveform_params)
        waveform_params_temp.update(waveform_params_template)
        if not template_params.get('Lframe', False):
            template_params_Lframe = lisatools.convert_SSBframe_to_Lframe(
                                template_params,
                                t0=waveform_params_temp['t0'],
                                frozenLISA=waveform_params_temp['frozenLISA'])
        else:
            template_params_Lframe = template_params.copy()
        # Compute sa_lm, se_lm for template
        sa_lm, se_lm = lisatools.sae_lm(template_params_Lframe, self.dist_inj, modes=self.modes)
        delta_sa = sa_lm - self.sa_lm_inj
        delta_se = se_lm - self.se_lm_inj
        # Loglikelihood, Eq. (68) in arXiv:2003.00357
        #lnL = 0.
        # for i,lm in enumerate(self.modes):
        #     for j,lpmp in enumerate(self.modes[i:]):
        #         # Diagonal terms -- <lm|lm> should be real, but rounding errors
        #         if i==j:
        #             lnL += -1./2 * np.real(self.scaled_innerproduct['matrix'][i,i]) * (np.abs(sa_lm[lm] - self.sa_lm_inj[lm])**2 + np.abs(se_lm[lm] - self.se_lm_inj[lm])**2)
        #         # Cross terms
        #         else:
        #             lnL += -1./2 * 2*np.real(self.scaled_innerproduct['matrix'][i,j] * (sa_lm[lm] - self.sa_lm_inj[lm]) * np.conj(sa_lm[lpmp] - self.sa_lm_inj[lpmp]) + (se_lm[lm] - self.se_lm_inj[lm]) * np.conj(se_lm[lpmp] - self.se_lm_inj[lpmp]))
        # for i,lm in enumerate(self.modes):
        #     for j,lpmp in enumerate(self.modes):
        #         lnL += -1./2 * self.scaled_innerproduct['matrix'][i,j] * ((sa_lm[lm] - self.sa_lm_inj[lm]) * np.conj(sa_lm[lpmp] - self.sa_lm_inj[lpmp]) + (se_lm[lm] - self.se_lm_inj[lm]) * np.conj(se_lm[lpmp] - self.se_lm_inj[lpmp]))
        lnL = -1./2 * np.dot(delta_sa, np.dot(self.scaled_innerproduct['matrix'], np.conj(delta_sa))) -1./2 * np.dot(delta_se, np.dot(self.scaled_innerproduct['matrix'], np.conj(delta_se)))
        lnL = np.real(lnL)

        return lnL

class LikelihoodLISASMBH:
    """Likelihood class for LISA SMBH, single-source.
    """

    def __init__(self, source_params, **waveform_params):
        """Constructor
        Args:
          source_params         # Dictionary of the source parameters
        Keyword args:
          waveform_params       # Waveform options and extra parameters
        """

        self.waveform_params = copy.deepcopy(waveform_params_smbh_default)
        self.waveform_params.update(waveform_params)
        # Dictionary update is shallow... we have to re-deepcopy
        self.waveform_params = copy.deepcopy(self.waveform_params)

        # If source params are given in the Lframe, convert to SSB
        if source_params.get('Lframe', False):
            self.source_params_SSBframe = lisatools.convert_Lframe_to_SSBframe(
                                source_params,
                                t0=self.waveform_params['t0'],
                                frozenLISA=self.waveform_params['frozenLISA'])
            self.source_params_Lframe = source_params.copy()
        else:
            self.source_params_SSBframe = source_params.copy()
            self.source_params_Lframe = lisatools.convert_SSBframe_to_Lframe(
                                source_params,
                                t0=self.waveform_params['t0'],
                                frozenLISA=self.waveform_params['frozenLISA'])

        # Necessary back-and-forth between strings and C enums
        # Convert in-place LISA constellation from string to dict
        # LISAconst = pyresponse.LISAconstDict[self.waveform_params['LISAconst']]
        # self.waveform_params['LISAconst'] = LISAconst
        # Convert in-place LISA instrumental noise from string to internal enum
        # LISAnoise = self.waveform_params['LISAnoise']
        # LISAnoise['InstrumentalNoise'] = pyLISAnoise.LISAnoiseDict[
        #                                         LISAnoise['InstrumentalNoise']]

        # Generate injection signal -- takes SSB frame params as input
        self.tdisignal_inj = GenerateLISATDISignal_SMBH(
                                                    self.source_params_SSBframe,
                                                    **self.waveform_params)

    def lnL(self, template_params, **waveform_params_template):
        waveform_params_temp = copy.deepcopy(self.waveform_params)
        waveform_params_temp.update(waveform_params_template)
        if template_params.get('Lframe', False):
            template_params_SSBframe = lisatools.convert_Lframe_to_SSBframe(
                                template_params,
                                t0=waveform_params_temp['t0'],
                                frozenLISA=waveform_params_temp['frozenLISA'])
        else:
            template_params_SSBframe = template_params.copy()
        template_params_SSBframe = pytools.complete_mass_params(template_params_SSBframe)
        template_params_SSBframe = pytools.complete_spin_params(template_params_SSBframe)
        if not pytools.check_physical_params(template_params_SSBframe):
            return -1e99
        lnL = LogLikelihoodLISA_SMBH(template_params_SSBframe,
                                          self.tdisignal_inj,
                                          **waveform_params_temp)
        return lnL

class LikelihoodLISASOBH:
    """Likelihood class for LISA SOBH, single-source.
    """

    def __init__(self, source_params, **waveform_params):
        """Constructor
        Args:
          source_params         # Dictionary of the source parameters
        Keyword args:
          waveform_params       # Waveform options and extra parameters
        """

        self.source_params = copy.deepcopy(source_params)
        self.source_params = pytools.complete_mass_params(self.source_params)
        self.waveform_params = copy.deepcopy(waveform_params_sobh_default)
        self.waveform_params.update(waveform_params)
        # Dictionary update is shallow... we have to re-deepcopy
        self.waveform_params = copy.deepcopy(self.waveform_params)

        # Lframe is not implemented for SOBH (as it is tied to the merger)
        if source_params.get('Lframe', False):
            raise ValueError('Lframe not supported for SOBH.')

        # Generate injection signal -- takes SSB frame params as input
        self.tdisignal_inj = GenerateLISATDISignal_SOBH(
                                                    self.source_params,
                                                    **self.waveform_params)

    def lnL(self, template_params, **waveform_params_template):
        waveform_params_temp = copy.deepcopy(self.waveform_params)
        waveform_params_temp.update(waveform_params_template)
        if template_params.get('Lframe', False):
            raise ValueError('Lframe not supported for SOBH.')
        template_params = pytools.complete_mass_params(template_params)
        template_params = pytools.complete_spin_params(template_params)
        if not pytools.check_physical_params(template_params):
            return -1e99
        lnL = LogLikelihoodLISA_SOBH(template_params,
                                          self.tdisignal_inj,
                                          **waveform_params_temp)
        return lnL

class LikelihoodLISASOBH_AGN:
    """Likelihood class for LISA SOBH AGN, single-source.
    """

    def __init__(self, source_params, **waveform_params):
        """Constructor
        Args:
          source_params         # Dictionary of the source parameters
        Keyword args:
          waveform_params       # Waveform options and extra parameters
        """

        self.source_params = copy.deepcopy(source_params)
        self.source_params = pytools.complete_mass_params(self.source_params)
        self.waveform_params = copy.deepcopy(waveform_params_sobh_default)
        self.waveform_params.update(waveform_params)
        # Dictionary update is shallow... we have to re-deepcopy
        self.waveform_params = copy.deepcopy(self.waveform_params)

        # Lframe is not implemented for SOBH (as it is tied to the merger)
        if source_params.get('Lframe', False):
            raise ValueError('Lframe not supported for SOBH.')

        # Generate injection signal -- takes SSB frame params as input
        self.tdisignal_inj = GenerateLISATDISignal_SOBH_AGN(
                                                    self.source_params,
                                                    **self.waveform_params)

    def lnL(self, template_params, **waveform_params_template):
        waveform_params_temp = copy.deepcopy(self.waveform_params)
        waveform_params_temp.update(waveform_params_template)
        if template_params.get('Lframe', False):
            raise ValueError('Lframe not supported for SOBH.')
        template_params = pytools.complete_mass_params(template_params)
        template_params = pytools.complete_spin_params(template_params)
        if not pytools.check_physical_params(template_params):
            return -1e99
        lnL = LogLikelihoodLISA_SOBH_AGN(template_params,
                                          self.tdisignal_inj,
                                          **waveform_params_temp)
        return lnL

################################################################################
# Linearly interpolated residuals likelihood
# TODO: move to specific file ?

def reweight_tdi(tdi, w):
    tdiw = {}
    tdiw['modes'] = copy.deepcopy(tdi['modes'])
    for lm in tdi['modes']:
        tdiw[lm] = {}
        tdiw[lm]['freq'] = tdi[lm]['freq'].copy()
        tdiw[lm]['phase'] = tdi[lm]['phase'].copy()
        for chan in ['chan1', 'chan2', 'chan3']:
            tdiw[lm]['amp_real_'+chan] = w[chan] * tdi[lm]['amp_real_'+chan]
            tdiw[lm]['amp_imag_'+chan] = w[chan] * tdi[lm]['amp_imag_'+chan]
    return tdiw

# Assumes identical freqs, modes for tdiw, tdiw0, phasew
def residual_tdi(tdiw, tdiw0, phasew0):
    resid = {}
    resid['modes'] = copy.deepcopy(tdiw0['modes'])
    for lm in tdiw0['modes']:
        resid[lm] = {}
        resid[lm]['freq'] = tdiw0[lm]['freq'].copy()
        expphase = np.exp(1j*tdiw[lm]['phase'])
        for chan in ['chan1', 'chan2', 'chan3']:
            resid[lm][chan] = ((tdiw[lm]['amp_real_'+chan]+1j*tdiw[lm]['amp_imag_'+chan]) * expphase * phasew0[lm] - (tdiw0[lm]['amp_real_'+chan]+1j*tdiw0[lm]['amp_imag_'+chan]))
    return resid

class LikelihoodLISASMBH_LinearResiduals:
    """Likelihood class for LISA SMBH from residuals, single-source.
    """

    def __init__(self, source_params, ngrid=128, minf_grid=None, maxf_grid=None, freq_array=None, **waveform_params):
        """Constructor
        Args:
          source_params         # Dictionary of the source parameters
        Keyword args:
          ngrid                 # Number of log-spaced points for linear interp.
                                  of residuals (default 128)
          minf_grid             # Min frequency of the grid (Hz, default None
                                  for auto-set)
          maxf_grid             # Max frequency of the grid (Hz, default None
                                  for auto-set)
          freq_array            # Full frequency array (default None, auto-set)
          waveform_params       # Waveform options and extra parameters
        """

        self.waveform_params = copy.deepcopy(waveform_params_smbh_default)
        self.waveform_params.update(waveform_params)
        # Dictionary update is shallow... we have to re-deepcopy
        self.waveform_params = copy.deepcopy(self.waveform_params)

        # We have to impose the same frequency grid for all modes
        if 'scale_freq_hm' in self.waveform_params.keys() and self.waveform_params['scale_freq_hm']:
            raise ValueError('Linear residuals likelihood requires scale_freq_hm=False.')
        self.waveform_params['scale_freq_hm'] = False

        source_params = pytools.complete_mass_params(source_params)

        # If source params are given in the Lframe, convert to SSB
        if source_params.get('Lframe', False):
            self.source_params_SSBframe = lisatools.convert_Lframe_to_SSBframe(
                                source_params,
                                t0=self.waveform_params['t0'],
                                frozenLISA=self.waveform_params['frozenLISA'])
            self.source_params_Lframe = source_params.copy()
        else:
            self.source_params_SSBframe = source_params.copy()
            self.source_params_Lframe = lisatools.convert_SSBframe_to_Lframe(
                                source_params,
                                t0=self.waveform_params['t0'],
                                frozenLISA=self.waveform_params['frozenLISA'])

        # Generate injection signal -- takes SSB frame params as input
        # self.tdisignal_inj = GenerateLISATDISignal_SMBH(
        #                                             self.source_params_SSBframe,
        #                                             **self.waveform_params)

        # Frequency grid for linear interpolation of residuals
        # minf for Newtonian approximation of timetomerger_max, maxf for Mf=0.4
        # TODO: same detailed frequency bounds as in signal generation ?
        m1 = self.source_params_SSBframe['m1']
        m2 = self.source_params_SSBframe['m2']
        M = self.source_params_SSBframe['M']
        Ms = M * pyconstants.MTSUN_SI
        DeltaT = self.waveform_params['timetomerger_max'] * pyconstants.YRSID_SI
        if ngrid is None:
            ngrid = 128
        if minf_grid is None:
            minf_grid = pytools.funcNewtonianfoft(m1, m2, DeltaT)
        if maxf_grid is None:
            maxf_grid = 0.4 / Ms
        if waveform_params['minf'] is not None:
            minf_grid = np.fmax(minf_grid, waveform_params['minf'])
        if waveform_params['maxf'] is not None:
            maxf_grid = np.fmin(maxf_grid, waveform_params['maxf'])
        deltaf = 1./(2*DeltaT)
        freq_grid = pytools.logspace(minf_grid, maxf_grid, ngrid)

        # Full frequency array to precompute weights
        # To avoid memory issues with large arrays, we use two different deltaf
        # For inspiral (r>20M) and late-inspiral-MRD (r<20M)
        # NOTE: Newtonian approximation only, and not consistent across modes
        # f_endinsp is 20M in Newtonian approximation
        if freq_array is None:
            f_endinsp = 1./np.pi * (1./20)**(3./2) / Ms
            DeltaT_insp = pytools.funcNewtoniantoff(m1, m2, minf_grid)
            DeltaT_mrd = pytools.funcNewtoniantoff(m1, m2, f_endinsp)
            freq_array = np.concatenate((np.arange(minf_grid, f_endinsp, step=1./(2*DeltaT_insp)), np.arange(f_endinsp, maxf_grid, step=1./(2*DeltaT_mrd)), np.array([maxf_grid])))
        Npt = len(freq_array)

        # Reference waveform
        wftdi0_grid = GenerateLISATDI_SMBH(source_params, gridfreq=freq_grid, **self.waveform_params)
        wftdi0_array = GenerateLISATDI_SMBH(source_params, gridfreq=freq_array, **self.waveform_params)
        tdi0_grid = GetCAmpPhaseTDI(wftdi0_grid)
        tdi0_array = GetCAmpPhaseTDI(wftdi0_array)
        modes = wftdi0_grid['modes']
        nmodes = len(modes)
        chans = ['chan1', 'chan2', 'chan3']
        nchan = 3

        # Phase weight exp(-i Phi0)
        phasew0_grid = {}
        phasew0_array = {}
        for lm in modes:
            phasew0_grid[lm] = np.exp(-1j*tdi0_grid[lm]['phase'])
            phasew0_array[lm] = np.exp(-1j*tdi0_array[lm]['phase'])

        # Noise and weighting sqrt(f/Sn)
        LISAnoise = self.waveform_params['LISAnoise']
        noise_evaluator = pyLISAnoise.initialize_noise(LISAnoise,
                             TDI=self.waveform_params['TDI'],
                             TDIrescaled=self.waveform_params['TDIrescaled'],
                             LISAconst=self.waveform_params['LISAconst'])
        SnA_grid, SnE_grid, SnT_grid = pyLISAnoise.evaluate_noise(LISAnoise, noise_evaluator, freq_grid)
        #SnA_array, SnE_array, SnT_array = pyLISAnoise.evaluate_noise(LISAnoise, noise_evaluator, freq_array)

        w_grid = {}
        w_grid['chan1'] = np.sqrt(freq_grid / SnA_grid)
        w_grid['chan2'] = np.sqrt(freq_grid / SnE_grid)
        w_grid['chan3'] = np.sqrt(freq_grid / SnT_grid)

        # w_array = {}
        # w_array['chan1'] = np.sqrt(freq_array / SnA_array)
        # w_array['chan2'] = np.sqrt(freq_array / SnE_array)
        # w_array['chan3'] = np.sqrt(freq_array / SnT_array)

        tdiw0_grid = reweight_tdi(tdi0_grid, w_grid)
        # tdiw0_array = reweight_tdi(tdi0_array, w_array)

        # Change of variable to u = ln(f/fstar)
        # Conventional frequency mid-band just to make sense of u=ln(f/fstar)
        fstar = 1e-3
        u_grid = np.log(freq_grid / fstar)
        diffu_grid = np.diff(u_grid)
        # diffu2_grid = 1./2 * np.diff(u_grid)**2
        # diffu3_grid = 1./3 * np.diff(u_grid)**3
        u_array = np.log(freq_array / fstar)
        # For masking purposes we need this diff to be the same size as the original data - simply put a 0 last
        diffu_array = np.zeros(Npt)
        diffu_array[:-1] = np.diff(u_array)

        w0 = np.zeros((ngrid-1, nmodes, nmodes), dtype=complex)
        w1 = np.zeros((ngrid-1, nmodes, nmodes), dtype=complex)
        w2 = np.zeros((ngrid-1, nmodes, nmodes), dtype=complex)
        for k in range(ngrid - 1):
            mask = (u_grid[k] <= u_array) & (u_array < u_grid[k+1])
            for i,lm in enumerate(modes):
                for j,lpmp in enumerate(modes):
                    # NOTE: phasew0 is exp(-I*phi), expdeltaphi=exp(I*(philm-philpmp))
                    expdeltaphi_interval = np.conj(phasew0_array[lm][mask]) * phasew0_array[lpmp][mask]
                    u_array_interval = u_array[mask]
                    diffu_array_interval = diffu_array[mask]
                    w0[k,i,j] = np.sum(diffu_array_interval * expdeltaphi_interval)
                    w1[k,i,j] = np.sum(diffu_array_interval * (u_array_interval - u_grid[k]) * expdeltaphi_interval)
                    w2[k,i,j] = np.sum(diffu_array_interval * (u_array_interval - u_grid[k])**2 * expdeltaphi_interval)

        self.modes = modes
        self.nmodes = nmodes
        self.chans = chans
        self.nchan = nchan
        self.ngrid = ngrid
        self.freq_grid = freq_grid
        self.minf_grid = minf_grid
        self.maxf_grid = maxf_grid
        self.u_grid = u_grid
        self.diffu_grid = diffu_grid
        self.wftdi0_grid = wftdi0_grid
        self.tdiw0_grid = tdiw0_grid
        self.phasew0_grid = phasew0_grid
        self.w_grid = w_grid
        self.w0 = w0
        self.w1 = w1
        self.w2 = w2

    def lnL(self, template_params, **waveform_params_template):
        # TODO: we allow for waveform_params_template, but many options must be
        # the same (mode content, ...) -- should we check for compatibility ?
        waveform_params_temp = copy.deepcopy(self.waveform_params)
        waveform_params_temp.update(waveform_params_template)
        # We have to impose the same frequency grid for all modes
        # NOTE: we do not check whether 'scale_freq_hm' is given in wf params
        waveform_params_temp['scale_freq_hm'] = False
        if template_params.get('Lframe', False):
            template_params_SSBframe = lisatools.convert_Lframe_to_SSBframe(
                                template_params,
                                t0=waveform_params_temp['t0'],
                                frozenLISA=waveform_params_temp['frozenLISA'])
        else:
            template_params_SSBframe = template_params.copy()

        template_params_SSBframe = pytools.complete_mass_params(template_params_SSBframe)
        template_params_SSBframe = pytools.complete_spin_params(template_params_SSBframe)
        if not pytools.check_physical_params(template_params_SSBframe):
            return -1e99

        # Generate template waveform and reweight
        wftdi_grid = GenerateLISATDI_SMBH(template_params, gridfreq=self.freq_grid, **waveform_params_temp)
        tdi_grid = GetCAmpPhaseTDI(wftdi_grid)
        tdiw_grid = reweight_tdi(tdi_grid, self.w_grid)

        # Compute residuals
        resid_grid = residual_tdi(tdiw_grid, self.tdiw0_grid, self.phasew0_grid)

        # Constant and linear-in-u terms of residuals, put in an array
        alpha0 = np.zeros((self.ngrid-1, self.nmodes, self.nchan), dtype=complex)
        alpha1 = np.zeros((self.ngrid-1, self.nmodes, self.nchan), dtype=complex)
        for i,lm in enumerate(self.modes):
            for l,chan in enumerate(self.chans):
                alpha0[:,i,l] = resid_grid[lm][chan][:-1]
                alpha1[:,i,l] = np.diff(resid_grid[lm][chan]) / self.diffu_grid

        # Norm of linearly interp. residuals, sum over grid, modes, channels
        resid_norm = pytools.linear_interp_multimode_3chan_resid_norm(alpha0, alpha1, self.w0, self.w1, self.w2)

        lnL = -1./2 * resid_norm
        return lnL
