# Source code for waveformtools.waveformtools

    # pylint: disable=C0301
    """  This module contains a set of tools to handle data from numerical relativity simulations and gravitational waves.

    """

    import datetime
    import os
    import pickle

    # import statistics
    import sys
    import traceback
    from inspect import getframeinfo, stack
    import config
    import numpy as np

    from numba import njit

    # from scipy import signal

    try:
        import pycbc
    except Exception as excep:
        print(excep)

    import scipy

    # matplotlib.use('Agg')
    from matplotlib import pyplot as plt
    from termcolor import colored

    ##########################################################################
    """ Basic functions """
    ##########################################################################

    def message(
        *args, message_verbosity=2, print_verbosity=config.print_verbosity, log_verbosity=config.log_verbosity, **kwargs
    ):
        """The print function with verbosity levels and logging facility.

        Notes
        -----

        Verbosity choices:

                        message_verbosity   :   Each message carries with it a verbosity level.
                                                More the verbosity less the priority.
                                                Default value is 2 i.e. informational.

                        print_verbosity     :   prints all messages above this level of verbosity.

                        log_verbosity       :   logs all messages above this level of verbosity.

        Verbosity  levels:

                        0: Errors

                        1: Warnings

                        2: Information

        Parameters
        ----------

                        ``*args`            :   non-keyword arguments
                                                same arguments as to that of the print functions,

                        message_verbosity   :   int

                        print_verbosity     :   int

                        log_verbosity       :   int

                        ``**kwargs``        :   keyword arguments
                                                Same as that of the print function.

        Returns
        -------

                        1                   :   int
                                                messages to stdout and logging of messages,
                                                while the function returns 1.
        """

        # If message verbosity matches the global verbosity level, then print
        if message_verbosity <= print_verbosity:
            print(*args, **kwargs)
        if log_verbosity <= message_verbosity:
            now = str(datetime.datetime.now())
            tstamp = (now[:10] + "_" + now[11:16]).replace(":", "-")
            caller = getframeinfo(stack()[1][0])
            # frameinfo = getframeinfo(currentframe())
            if not os.path.isdir("logs"):
                os.mkdir("logs")

            with open("logs/" + tstamp + ".log", "a") as log_file:
                if message_verbosity == -1:
                    for line in traceback.format_stack():
                        log_file.write(line.strip())
                log_file.write("\n")
                log_file.write("{}:{}\t{}".format(caller.filename, caller.lineno, *args))
                log_file.write("\n")
        return 1

    def mode(a_list):
        """Find the mode of a list

        Parameters
        ----------
        a_list: list
                The list whose mode is to be found

        Returns
        -------
        list_mode:  float
                    The mode of the list.

        """

        a_list = list(a_list)

        return max(set(a_list), key=a_list.count)

    ##########################################################################
    """ Data handling functions """
    ##########################################################################
    """ This library consists of a bunch of functions that were frequently used
    for handling NR data. """

    """ Notes
    1. Some functions here display plots. If you intend to use these on the
    cluster and if xterm is not adequately setup, you may have to
    either comment the plot codes or use the`Agg` mode for plotting
    and save the figures instead of showing by changing the appropriate
    lines in the code.

    2. These is a module consisting of functions, not completely optimized
    for speed. This will happen in future.
    3. These functions are not defined in classes as they mostly use and
    operate on the objects of pycbc's builtin classes.
    4. Any suggestions, comments, critisism invited to vaishak@gmail.com!"""

    # Data I/O functions

    def save_obj(obj, name, obj_dir="./", protocol=pickle.HIGHEST_PROTOCOL):
        """A function to save python objects to disk using pickle.

        Parameters
        ----------

        obj                     :   object
                                                                The python object to be saved.
        name                    :   string
                                                                The filename.
        obj_dir                 :   string
                                                                The path to directory to be saved in. Defaults to PWD
        protocol                :   int
                                                                The protocol to be used to save. Default is binary.

        Notes
        -----
        Protocols:

        0                       : Text
        5                       : Binary

        See the man page of pickle for more details.

        Returns
        -------

        Nothing (other than saving the data to the disk).

        """

        # Create the directory dir if it doesn't exist.
        if not os.path.isdir(obj_dir):
            os.mkdir(obj_dir)

        # Pickle the file to disk.
        with open(obj_dir + name + ".pkl", "wb") as out_file:
            pickle.dump(obj, out_file, protocol)

    def load_obj(name, obj_dir="./"):
        """A function to load python objects from the disk using pickle.

        Parameters
        ----------

        name:   string
                                        The filename.
        obj_dir :   string
                                                        The path to directory in which file exists. Defaults to PWD.

        Returns
        -------

        obj:    object
                                        A python object with the contents of the file.
        """

        # Load the pickled data
        with open(obj_dir + name + ".pkl", "rb") as data_file:
            return pickle.load(data_file)

    def removeNans(xdata, ydata):
        """Remove Nans from (xdata,ydata) data pair.
        Removes Nans in xdata and ydata and the
        corresponding y and x entries.

        Parameters
        ----------

        xdata:  1d array
                        The x axis of the data.
        ydata : 1d array
                        The y axis of the data.

        Returns
        -------

        x_no_nan:   1d array
        y_no_nan:   1d,array
                        The data pair x,y with Nans removed.

        """

        # Find the location of x Nans to be removed.
        nan_locs = np.where(np.isnan(xdata))[0]
        # Remove the xdata and the corresponding y entries.
        xdata = np.delete(xdata, nan_locs)
        ydata = np.delete(ydata, nan_locs)
        # Find the location of the y Nans to be removed.
        nan_locs = np.where(np.isnan(ydata))[0]
        # Remove the y and the correspoinding x entries.
        xdata = np.delete(xdata, nan_locs)
        ydata = np.delete(ydata, nan_locs)
        # Find the location of the inifinities to be removed along x.
        inf_locs = np.where(np.isinf(xdata))[0]
        # Remove the x and the correspoiding y entries.
        xdata = np.delete(xdata, inf_locs)
        ydata = np.delete(ydata, inf_locs)
        # Find the location of the infinities to be removed along y.
        inf_locs = np.where(np.isinf(ydata))[0]
        # Remove the y and the corresponding x locations.
        x_no_nan = np.delete(xdata, inf_locs)
        y_no_nan = np.delete(ydata, inf_locs)

        # Return the reconditioned x and y data.
        return x_no_nan, y_no_nan

    def differentiate(data, delta_t=None, TS=False):
        """Differentiate a timeseries in time domain using the Simple Euler method

        Parameters
        ----------
        data:   1d array \\ a pycbc TimeSeries object
                                        The time series to be differentiated.
        delta_t:    float
                    The grid spacing delta_t.
                    Supplying delta_t overrides
                    the delta_t attribute of TimeSeries.

        Returns
        -------
        ddt_data:   pycbc TimeSeries object
                    The differentiated 1d data
                    as pycbc TimeSeries"""

        if not delta_t:
            try:
                delta_t = data.delta_t
            except BaseException:
                message("Input is not a TimeSeries. Please supply gridspacing as delta_t", message_verbosity=0)

        dydx = np.diff((np.array(data))) / delta_t

        if TS is True:
            dydx = pycbc.types.timeseries.TimeSeries(dydx, delta_t)

        return dydx

    def integrate_first_order(data, t_start=None, t_end=None, delta_t=None, to_taper=False, TS=False):
        """Integrate a timeseries using first order method.

        Notes
        -----
        Capabilities

        Simple Euler integrator, option to taper the ends.

        Parameters
        ----------

        data:   1d array or a pycbc TimeSeries object
                The input function to be integrated.
        delta_t:    float
                    The grid spacing delta_t,
        to_taper:   bool
                    True or False. Whether or not
                    to taper the input series.

        Returns
        -------

        int_data:   a pycbc TimeSeries object
                    TimeSeries of the time integrated data"""

        # Check if object is pycbc timeseries. Recover delta_t, t_start, t_end if yes.
        if not delta_t:
            try:
                # Find sampling time_step
                delta_t = data.delta_t
                data_time = data.sample_times
                t_start_dat, t_end_dat = data_time[0], data_time[-1]

            except BaseException:
                message(
                    "Input is not a TimeSeries." "Please input a pycbc TimeSeries" "or supply gridspacing as delta_t",
                    message_verbosity=0,
                )
        else:
            t_start_dat = 0
            t_end_dat = len(data) * delta_t

        if not t_start:
            t_start = t_start_dat

        if not t_end:
            t_end = t_end_dat

        data = np.array(data)

        # Revert t_start and t_end to t_start_dat and t_end_dat i.e. to the
        # starting time and  duration of the data respectively if user specified
        # t_start is shorter and t_end is longer than t_start + the duration of
        # the data.
        t_start = max(t_start, t_start_dat)
        t_end = min(t_end, t_end_dat)

        start_index = int((t_start - t_start_dat) / delta_t)
        end_index = int((t_end - t_start_dat) / delta_t)

        if to_taper:
            data = taper(data, delta_t)

        integdat = np.zeros([end_index - start_index])

        integdat[0] = 0.0  # dat[0]*delta_t
        # totinteg = np.sum(np.array(data)) * delta_t # commented on May 5 2022 to
        # conform with pep8 style. Not sure of it's use.

        # mean = np.mean(dat)

        data = np.array(data)

        for i in range(1, end_index - start_index):
            integdat[i] = integdat[i - 1] + (data[start_index + i - 1]) * delta_t

        if TS is True:
            integdat = pycbc.types.timeseries.TimeSeries(integdat, delta_t, epoch=t_start)

        return integdat

    def compute_frequencies(t_coal, t_val, chirp_mass):
        """Compute the Newtonian instantaneous frequency
        of strain waveform from coalescence time and
        chirp mass, from the Finn-Chernoff model.

        Parameters
        ----------

        t_coal: float
                                The coalescence time,
        t_val:  1d array
                        The time,
        chirp_mass: float
                                        The chirpmass.

        Returns
        -------

        freqs:  float
                        The instantaneous frequency of the strain waveform."""
        freqs = (1.0 / (np.pi * chirp_mass)) * (5.0 / 256) ** (3.0 / 8) * (chirp_mass / (t_coal - t_val)) ** (3.0 / 8)

        return freqs

    def totalmass(mass_ratio, chirp_mass):
        """Find total mass from mass ratio and chirpmass.

        Parameters
        ----------

        mass_ratio: float
                                        The mass ratio of the system.
        mchirp: float
                                The chirp mass of the system.

        Returns
        -------

        total_mass: float
                                        The total mass of the system."""

        return (chirp_mass * (1.0 + mass_ratio) ** (6.0 / 5)) / mass_ratio ** (3.0 / 5)

    def massratio(chirp_mass):
        """Compute the mass ratio from chirpmass. Assumes total mass to be 1.

        Parameters
        ----------

        mchirp: float
                                The chirp mass of the system.

        Returns
        -------

        mass_ratio: float
                                        The Mass ratio of the system
        """
        mass_ratio = (
            chirp_mass ** (1.0 / 3) - 2.0 * chirp_mass**2.0 - np.sqrt(chirp_mass ** (2.0 / 3) - 4.0 * chirp_mass ** (7.0 / 3))
        ) / (2.0 * chirp_mass**2.0)

        return mass_ratio

    # Defining function for calculating Chirpmass from a2

    def compute_chirp_mass(a2_param):
        """Compute the chirpmass from a2, the coefficient of time of the Finn-Chernoff waform model.

        Parameters
        ----------

        a2_param:   1d array \\ float
                                The a2 parameter in the Finn Chernoff model.

        Returns
        -------

        chirp_mass: float
                                        The chirp mass
        """
        chirp_mass = 2 ** (8.0 / 5) / (5 * np.array(a2_param) ** (8.0 / 5))
        return chirp_mass

    def lengtheq(data_a, data_b, delta_t=None, is_ts=False):
        """Equalize the length of two timeseries/array by
        appending zeros at the end of the array. No tapering.

        Procedure
        ---------

        1. Check if input data are timeseries.
           If not then construct timeseries using delta_t.
        2. Check data length.
           If they are already equal then skip and return the inputs.
        3. Check if data_a is smaller then data_b ( or vice versa).
           Then augment zeroes at either ends of array to data_a
           (data_b) and return.

        Parameters
        ----------
        data_a: list
                                The input waveform A
        data_b: list
                                The input waveform B.
        delta_t:    float
                                The time steping. Defaults to `None`.
        is_ts:  bool
                To determine whether the given data
                is a pycbc TimeSeries.
        Returns
        -------

        equalized_signals : list
                            The Tapered, length equalized waveforms data_a and data_b,
                            and a flag denoting which waveform was changed, `a` or `b`.

        Notes
        -----
        Recommended usage:

        Change length of waveform `a` to match with that of waveform `b`.

        """

        # Check if input data vectors are pycbc TimeSeries. If yes, create a copy
        # of data and extract delta_t.
        # ts = isinstance(data_a, pycbc.types.timeseries.Timeseries)
        # and isinstance(data_b, pycbc.types.timeseries.Timeseries)

        if is_ts:
            if not delta_t:
                signala = data_a
                signalb = data_b

                try:
                    delta_t = signala.delta_t

                except AttributeError:
                    try:
                        delta_t = signalb.delta_t
                    except BaseException:
                        message(
                            "Input is not a TimeSeries."
                            "Please supply a pycbc TimeSeries object"
                            "or the gridspacing as delta_t",
                            message_verbosity=0,
                        )
        else:
            delta_t = 1

        # If not TimeSeries, then construct TimeSeries using delta_t.
        data_a = np.array(data_a)
        data_b = np.array(data_b)

        signala = data_a
        signalb = data_b
        # signala = pycbc.types.timeseries.TimeSeries(data_a, delta_t)
        # signalb = pycbc.types.timeseries.TimeSeries(data_b, delta_t)

        # If the length is already equal, then skip.
        if len(data_a) == len(data_b):
            lflag = "ab"

        # If data_a < data_b
        elif len(data_a) < len(data_b):
            # add zeros to data_a when a is smaller
            lflag = "a"
            zers = len(data_b) - len(data_a)
            signala = np.transpose(np.concatenate((np.transpose(data_a), np.transpose(np.zeros([zers])))))
            # signala = pycbc.types.timeseries.TimeSeries(signala, delta_t)
            # return pycbc.types.timeseries.TimeSeries(signalb,delta_t),lflag
        # If data_b < data_a
        else:
            # print("Error!")
            # add zeros to b when b is smaller
            lflag = "b"
            zers = len(data_a) - len(data_b)
            signalb = np.transpose(np.concatenate((np.transpose(data_b), np.transpose(np.zeros([zers])))))
            # signalb = pycbc.types.timeseries.TimeSeries(signalb, delta_t)
            # return pycbc.types.timeseries.TimeSeries(signala,delta_t),lflag

        # Returns a list containing the length equalized arrays and the flag.
        if not is_ts:
            signala = np.array(signala)
            signalb = np.array(signalb)

        return [signala, signalb, lflag]

    def taperlengtheq(data_a, data_b, delta_t=None):
        """Taper and equalize the lengths of two arrays.

        Parameters
        ----------

        data_a: list
                                                        The input waveform A.
        data_b: list
                                                        The input waveform B.
        delta_t:    float
                                        The time steping.

        Returns
        -------

        equalized_signals:  list
                            The Tapered, length equalized waveforms
                            data_a and data_b, and a flag denoting
                            which waveform was changed, `a` or `b`.
        """

        # Check if input data is pycbc TimeSeries. If yes, then ectract delta_t.
        if not delta_t:
            signala = data_a
            signalb = data_b

            try:
                delta_t = signala.delta_t

            except AttributeError:
                try:
                    delta_t = signalb.delta_t
                except BaseException:
                    message(
                        "Input is not a TimeSeries." "Please supply a pycbc TimeSeries object " "or the gridspacing as delta_t",
                        message_verbosity=0,
                    )

        # Ensure data is numpy array.
        signalb = np.array(data_b)
        # Taper waveform A.
        signala = np.array(taper(data_a, delta_t))

        # equalize the length of a to match with b and return the length equalized
        # arrays.
        return lengtheq(signala, signalb, delta_t)

    # Check data for discontinuity

    def iscontinuous_old(timeaxis, delta_t=0):
        """Check if the data has discontinuities. This checks for repetitive time rows and jumps.

        Notes
        -----
        Types of discontunuities

        0: Continuous.
        1: Repetitive rows.
        2: Jumps in timeaxis.

        Parameters
        ----------

        timeaxis:   list
                    Input as a single 1d time axis
                    or a list of 1d arrays [time, data1, data2, ...].
                    All the data share the common time axis `time`
        delta_t:    float
                    The time stepping.
        toldt : float
                The tolerance for error in checking.
                Defaluts to toldt=1e-3.

        Returns
        -------

        discontinuity_details : a list.
                                It contains: [ the actual location of discontinuity along
                                the time axis,
                                value of time location of original array,
                                the type of discontinuity].

        """

        # If data array is supplied, assign first column as timeaxis
        if np.array(timeaxis).ndim > 1:
            timeaxis = timeaxis[:, 0]
        # Check data for continuity.
        # If not timeseries
        if delta_t == 0:
            delta_t = timeaxis[1] - timeaxis[0]
        # Set epoch to first element of timeaxis
        epoch = timeaxis[0]
        # List to hold discintinuity details
        discontinuity_details = []
        # List to hold indices
        # indices = []
        # Set start index,epoch_index to 0
        # index = 0
        epoch_index = 0
        # List to hold discontinuity type
        discontinuity_type = []

        for timestamp in timeaxis:
            # Iterate over every timestamp in timeaxis
            # Check for discontinuity
            if timestamp != timeaxis[0] + epoch_index * delta_t:
                # Check for type of discontinuity
                # Repetitive rows
                if timestamp < timeaxis[0] + epoch_index * delta_t:
                    discontinuity_type = 1
                # Missing rows
                elif epoch > timeaxis[0] + epoch_index + delta_t:
                    discontinuity_type = 2
                # Append [index location,correct timestamp
                discontinuity_details.append([epoch_index, timeaxis[epoch_index], discontinuity_type])
            epoch_index += 1
            # message("Progress: %f%%\r"%(epoch_index*100./len(timeaxis)))

        # Print the result
        # changed discontinuity_timestamps to discontinuity details May 5 2022
        if len(discontinuity_details) != 0:
            message("The data is discontinuous!")
        else:
            message("The data is continuous!")
        # Return the details of discontinuity
        return discontinuity_details

    # Check data for discontinuity

    def iscontinuous(data, delta_t=0, toldt=1e-3):
        """Check if the data has discontinuities. This checks for repetitive time rows and jumps.

        Notes
        -----
        Types of discontunuities

        0: Continuous.
        1: Repetitive rows.
        2: Jumps in timeaxis.

        Parameters
        ----------

        data:   list
                                        Input as a list of 1d arrays [time, data1, data2, ...].
                                        All the data share the common time axis `time`
        delta_t:    float
                                        The time stepping.
        toldt : float
                                        The tolerance for error in checking. defaluts to toldt=1e-3.

        Returns
        -------

        discontinuity_details:  a list.
                                It contains:
                                1. A list. details of discontinuity:  index location of original array,
                                                                    the corresponding discinbtinuity type.
                                2. A float. the global discontinuity type.
        """

        # Check the data (time,datar,datai) for locations of repetition and
        # discontinuities.
        message("Checking continuity of data", message_verbosity=1)
        # Ensure data as numpy array
        data = np.array(data)
        message("Data shape:", (data.shape), message_verbosity=3)
        # Find if data contanins more than timeaxis
        axis = data.ndim - 1
        message("Axis:%d" % axis, message_verbosity=3)
        # Associate data[0] as timeaxis
        if axis > 0:
            shapes = data.shape
            if shapes[0] > shapes[1]:
                data = np.transpose(data)
            timeaxis = data[0, :]
            message("The time axis is :%s" % timeaxis, message_verbosity=3)
        else:
            timeaxis = data
        message("shape of data:", (data.shape), message_verbosity=3)

        # If data array is supplied, assign first column as timeaxis
        # if np.array(timeaxis).ndim>1:
        #        timeaxis=np.array([item[0] for item in timeaxis])
        # message('Timeaxis`,timeaxis,message_verbosity=3)
        # Check data for continuity.
        # If delta_t is not supplied
        if delta_t == 0:
            # delta_t = statistics.mode(np.diff(timeaxis))
            delta_t = mode(np.diff(timeaxis))

        # Set epoch to first element of timeaxis
        epoch = timeaxis[0]
        # List to hold discintinuity details
        discontinuity_details = []
        # List to hold indices
        # indices = []
        # Initialize start index,epoch_index to 0
        index = 0
        # Initialize epoch to start of timeaxis
        epoch = timeaxis[0]
        # Initialize epoch_index to 0
        epoch_index = 0
        # Repetition flag
        repetition = 0
        # Discountinuity flag
        discont = 0
        # Discontinuity type
        discont_type = 0
        # List to hold discontinuity type
        discont_type_details = []
        # Counters
        repetition_counter = 0
        discont_counter = 0
        # Iterate over every timestamp in timeaxis.
        # Note the type of discontinuity.
        # Ignore repetition and check for actual discontinuity i.e. missing rows.

        while index < len(timeaxis):
            # Read original timestamp at the location of operation number
            original_timestamp = timeaxis[index]
            # Calculate the recentered timestamp starting at epoch where previous
            # discontinuity was found.
            recentered_timestamp = epoch + (index - epoch_index) * delta_t
            # message(original_timestamp,recentered_timestamp)
            # Check for next discontinuity
            if abs(original_timestamp - recentered_timestamp) >= toldt * delta_t:
                # Reset epoch, epoch_index
                epoch = original_timestamp
                epoch_index = index
                # Check for type of discontinuity
                # Repetitive rows
                if (original_timestamp == recentered_timestamp) or (
                    recentered_timestamp - original_timestamp
                ) >= toldt * delta_t:
                    # if recentered_timestamp-original_timestamp<=0.01*delta_t:
                    #        message("Error!!",message_verbosity=0)
                    repetition = 1
                    discont_type = repetition
                    message(
                        "Repetitive rows found at index: %d,timestamp: %f" % (index, original_timestamp),
                        message_verbosity=3,
                    )
                    message(
                        "Repetition at timestamp original: %f correct %f\n" % (original_timestamp, recentered_timestamp),
                        message_verbosity=3,
                    )
                    repetition_counter += 1
                # Missing rows
                if (original_timestamp - recentered_timestamp) >= (1.0 + toldt) * delta_t:
                    discont = 2
                    discont_type = discont
                    message(
                        "Jump discountinuity in data found at index:%d,timestamp:%f" % (index, original_timestamp),
                        message_verbosity=2,
                    )
                    message("delta_t=%f" % delta_t, message_verbosity=1)
                    message(
                        "Jump at timestamp original: %f correct %f\n Dt = %f"
                        % (original_timestamp, recentered_timestamp, (original_timestamp - recentered_timestamp) / delta_t),
                        message_verbosity=1,
                    )
                    discont_counter += 1
                # message("timei: %f timef %f\n"%(timeaxis[index-1],timeaxis[index]),message_verbosity=3)

                # discont_type=repetition+discont
                discont_type_details.append([index, discont_type])
                # Append [index location,recentered timestamp
                # discontinuity_details.append([index,recentered_timestamp,discontinuity_type])
            # Increment the no. of total operations
            index += 1
            # message("Progress: %f%%\r"%(epoch_index*100./len(timeaxis)))
        # discont_type=(repetition+discont)
        # Print the result
        if discont_type:
            message("The data is not clean!", message_verbosity=1)
            global_discont_type = repetition + discont
            message("Discontinuity type:", global_discont_type, message_verbosity=1)
            if global_discont_type == 1:
                message("The data has repetitive rows at %d locations" % repetition_counter, message_verbosity=1)
            elif global_discont_type == 2:
                message("The data has %d discontinuities" % discont_counter, message_verbosity=1)
            else:
                message("The data has repetitive rows and is discontinious", message_verbosity=1)

            discontinuity_details = [discont_type_details, global_discont_type]
        else:
            message("Data is continuous", message_verbosity=2)
            discontinuity_details = [[0, 0], 0]
        # Return the details of discontinuity

        return discontinuity_details

    # cleaning data for repeatative values

    def cleandata_old(data, verbose=False):
        """Old version. Check the data (time,datar,datai) for repetetive rows and remove them.

        Parameters
        ----------

        data:   list
                                        Input as a list of 1d arrays [time, data1, data2, ...].
                                        All the data share the common time axis `time`
        verbose : bool
                                        A verbosity flag.

        Returns
        -------

        cleaned_data:   nd array
                        The cleaned data array with repetitive
                        rows and gaps (if bridge=True) removed
        """

        # Ensure data as numpy array
        data = np.array(data)
        if verbose == "yes":
            message("Data shape:", data.shape)
        # Set axis along which to remove
        axis = data.ndim - 1
        if verbose == "yes":
            message("Axis:", axis)
        # Associate data[0] as timeaxis
        if axis > 0:
            shapes = data.shape
            if shapes[0] > shapes[1]:
                data = np.transpose(data)
            time = data[0, :]
            if verbose == "yes":
                message("The time array:", time)
        else:
            time = data
        # Assign delta_t
        # delta_t = statistics.mode(np.diff(time))
        delta_t = mode(np.diff(time))

        if verbose:
            message("length,shape of data", len(data), data.shape)
        # Reassign data without time_array
        # data=data[:,1:]
        # message("++++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
        # message("Checking data for repetative rows...\n")
        # Index of ros to delete
        dind = []
        # Initial data length
        ki_index = len(time)
        # message("length of old array is %d\n" %ki_index)
        # Row iteration variable
        ii_index = 0
        # Flag to identify if any repetetive rows were found
        rep_row_index = 0
        # ci=0
        # rowcounter=np.zeros([])
        # Iterate over rows
        while ii_index < ki_index - 1:
            # Repetition condition :if the successive time stamp is less than or
            # equal to the present, delete the row.
            if (time[ii_index + 1] - time[ii_index]) <= -0.1 * delta_t:
                # Set flag to 1 if repetition condition is met.
                rep_row_index = 1
                if verbose == "yes":
                    message("found a repeating row at %d, time %f\n" % (ii_index + 1, time[ii_index + 1]))
                    message("timei: %f timef %f\n" % (time[ii_index], time[ii_index + 1]))
                # ci = ci+1
                # Delete the entire row
                time = np.delete(time, ii_index + 1)
                # data = np.delete(data,ii_index,1)
                # data = [np.delete(item,ii_index+1) for item in data]
                data = np.delete(data, ii_index + 1, axis)
                # Append the deleted index to the bookkeeping array
                dind.append(ii_index + 1)
                # If a row is deleted, step back the iter variable by one step
                ii_index = ii_index - 1
            # Advance the iter variable
            ii_index = ii_index + 1
            # Recalculate the array length
            ki_index = len(time)
        if verbose == "yes":
            if rep_row_index == 1:
                message("No. of points removed = %d\n" % len(dind))
                message("length of new array is %d\n" % len(time))
            else:
                message("No points removed\n")
            # message("++++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
        # Return the "cleaned" data matrix
        cleaned_data = data
        # cleaned_data=[time]
        # for item in data:
        #    cleaned_data.append(item)
        # cleaned_data.append(item for item in data)
        # Transpose the data back to original shape
        if shapes[0] > shapes[1]:
            cleaned_data = np.transpose(cleaned_data)
        return cleaned_data

    #################################################
    # cleaning data for repeatative values
    # ToDo: breakdown this function into sub methods
    #################################################

    def cleandata(data, toldt=1e-3, bridge="no"):
        """Check the data (time,datar,datai) for repetetive rows and remove them.

        Parameters
        ----------

        data:   list
                                        Input as a list of 1d arrays [time, data1, data2, ...].
                                        All the data share the common time axis `time`
        toldt : float
                                        The tolerance for error in checking. defaluts to toldt=1e-3.
        bridge : bool
                                        A bridge flag to decide whether or not to interpolate and
                                        resample to fill in jump discontinuities.

        Returns
        -------

        cleaned_data:   list
                        The cleaned data array with repetitive
                        rows and gaps (if bridge=True) removed.

        """

        # Check the data (time,datar,datai) for repetetive rows and remove them.
        # Ensure data as numpy array
        data = np.array(data)
        message("Data shape:", (data.shape), message_verbosity=3)
        # Set axis along which to remove
        axis = data.ndim - 1
        message("Axis:%d" % axis, message_verbosity=3)

        # Associate data[0] as timeaxis
        if axis > 0:
            shapes = data.shape
            if shapes[0] > shapes[1]:
                data = np.transpose(data)
            time = data[0, :]
            message("The time array:%s" % time, message_verbosity=3)
        else:
            time = data

        # delta_t = statistics.mode(np.diff(time))
        delta_t = mode(np.diff(time))

        message("shape of data:", (data.shape), message_verbosity=3)

        # Reassign data without time_array
        # data=data[:,1:]
        # message("++++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
        # message("Checking data for repetative rows...\n")
        # Index of ros to delete
        dind = []

        # Initial data length
        ki_index = len(time)

        # message("length of old array is %d\n" %ki)
        # Row iteration variable
        ii_index = 0

        # Flag to identify if any repetetive rows were found
        rep_row_index = 0

        # ci=0
        # rowcounter
        counter = 0

        # Iterate over rows
        while ii_index < ki_index - 1:
            # Repetition condition :if the successive time stamp is less than or
            # equal to the present, delete the row.
            if (time[ii_index + 1] == time[ii_index]) or (time[ii_index] - time[ii_index + 1]) >= toldt * delta_t:
                # if time[ii_index]-time[ii_index+1]<=0.01*delta_t:
                #        message("Error!!",message_verbosity=0)
                # Set flag to 1 if repetition condition is met.
                rep_row_index = 1
                counter += 1
                message("found a repeating row at %d, time %f\n" % (ii_index + 1, time[ii_index + 1]), message_verbosity=3)
                message("timei: %f timef %f\n" % (time[ii_index], time[ii_index + 1]), message_verbosity=3)
                # ci = ci+1
                # Delete the entire row
                time = np.delete(time, ii_index + 1, 0)
                # data = np.delete(data,ii_index,1)
                # data = [np.delete(item,ii_index+1) for item in data]
                data = np.delete(data, ii_index + 1, axis)
                # Append the deleted index to the bookkeeping array
                dind.append(ii_index + 1)
                # If a row is deleted, step back the iter variable by one step
                ii_index = ii_index - 1
            # Advance the iter variable
            ii_index = ii_index + 1
            # Recalculate the array length
            ki_index = len(time)

        if rep_row_index == 1:
            message("Repetitive rows were removed", message_verbosity=2)
            message("No. of rows removed = %d\n" % counter, message_verbosity=2)
            message("Length of new array is %d\n" % len(time), message_verbosity=3)

        else:
            message("No points removed\n", message_verbosity=2)
            # message("++++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
        # Return the "cleaned" data matrix
        cleaned_data = data

        if bridge and iscontinuous(cleaned_data)[-1] >= 2:
            message("The data will be interpolated to bridge the gaps", message_verbosity=2)

            # from scipy import interpolate

            # Interpolate the data to fill in the discontinuities
            t_final = time[-1]
            t_initial = time[0]

            # Find delta_t
            delta_t = time[1] - time[0]
            index = 1

            # If second row is repetitive (If its discontinuous, help!)
            while delta_t <= 0:
                delta_t = np.diff(time)[index]
                index += 1
            proper_timeaxis = np.arange(t_initial, t_final, delta_t)
            interp_data = []
            interp_data.append(proper_timeaxis)
            if axis > 0:
                from scipy.interpolate import interp1d

                for index in range(1, min(shapes[0], shapes[1])):
                    interp_datai = interp1d(time, data[index, :])
                    interp_data.append(interp_datai(proper_timeaxis))

            cleaned_data = np.array(interp_data)
            message("The data has been interpolated", message_verbosity=2)
        message("Cleaned!", message_verbosity=2)

        # cleaned_data=[time]
        # for item in data:
        #    cleaned_data.append(item)
        # cleaned_data.append(item for item in data)
        # Transpose the data back to original shape

        if axis > 0:
            if shapes[0] > shapes[1]:
                cleaned_data = np.transpose(cleaned_data)
        return cleaned_data

    def shiftmatched(hdat, ind, delta_t=None, is_ts=False):
        """Timeshift an array. IMP: After timeshifting, the original
        length of the array is retained by clipping last(first) when
        ind > 0(ind <0) `ind` number of points!!. Make sure the input array
        already has number of zeros z > ind (z<ind) initially at the end.

        Parameters
        ----------
        hdat:   1d array or a pycbc TimeSeries object
                                        The input waveform to be shifted in time.
        ind:    int
                                        The numper of places to shift the input waveform.
        delta_t:    int
                                        the grid spacing in time.

        Returns
        -------
        shifted_wf: a pycbc TimeSeries object
                    The waveform array of same length timeshifted by
                    `ind` units by prepending zeros.
        """

        if is_ts:
            if not delta_t:
                try:
                    delta_t = hdat.delta_t
                except BaseException:
                    message("Input is not a TimeSeries. Please supply gridspacing as delta_t", message_verbosity=0)

        if ind > 0:
            # ind>0 case for shifting array to the right
            # message hdat
            # Length of the data
            # l = len(hdat)
            # Array holding zeroes to be appended
            zeros = np.zeros([ind])
            # The shifted array
            shifted_wf = np.transpose(np.concatenate((np.transpose(zeros), np.transpose(hdat))))[:-ind]
            # message msig
            # message msig[:-ind]
            # Return the clipped, shifted timeseries

        elif ind < 0:
            # ind <0 case for shifting array to the left
            # Length of the data
            # l = len(hdat)
            # Array holding zeroes to be appended
            zeros = np.zeros([ind])
            # The shifted array
            shifted_wf = np.transpose(np.concatenate((np.transpose(hdat), np.transpose(zeros))))[ind:]
            # message msig
            # message msig[:-ind]
            # Return a timeseries

        else:
            shifted_wf = hdat

        if is_ts:
            shifted_wf = pycbc.types.timeseries.TimeSeries(shifted_wf, delta_t)

        return shifted_wf

    @njit
    def unwrap_phase(phi0):
        """Unwrap the phase by finding turning points in phi0.
        Finding turning points for unwrapping arctan2 function

        Parameters
        ----------
        phi0 : 1darray
               The wrapped phase which takes values in the
               range (0, 2pi).
        Returns
        -------
        phic : 1darray
               The unwrapped phase.

        """

        # Bookkeeping for upper (tpu) and lower (tpd) turning points
        tpu = []
        tpd = []
        # j = 0
        # k = 0
        # Upper turning point criterion
        for i in range(0, len(phi0) - 2):
            if phi0[i] > 5 and phi0[i + 1] < 1:
                tpu.append(i)
                # j = j+1
            # Lower turning point
            if phi0[i] < 1 and phi0[i + 1] > 5:
                tpd.append(i)
            # k = k+1

        # Trim any zeros in the array( Note: Unecessary)
        # tpu = np.trim_zeros(tpu)
        # tpd = np.trim_zeros(tpd)
        # Calculate the timestamp of the turning points
        # tput = delta_t * np.array(tpu)
        # tpdelta_t = delta_t * np.array(tpd)

        # Unwrapping the phase: Unwrap using the turning points

        # Variable for unwrapped phase
        phic = phi0
        # Iteration varible for turning points
        j = 0

        # Unwrap the upper turning points by adding 2pi for every 2p
        for i in range(0, len(tpu)):
            for j in range(int(tpu[i]) + 1, len(phic)):
                phic[j] = phic[j] + 2.0 * np.pi
        # Unwrap lower turning points by subtracting 2*pi for every tp
        for i in range(0, len(tpd)):
            for j in range(int(tpd[i]) + 1, len(phic)):
                phic[j] = phic[j] - 2.0 * np.pi

        return phic

    # Complex Phase-Amplitude representation of data
    def xtract_cphase(tsdata_p, tsdata_x, delta_t=None, to_plot=False):
        """Given real and imaginary parts of a complex timeseries, extract the phase of the waveform :arctan_(Img(data)/Re(data))

        Parameters
        ----------
        tsdata_p:   1d array / a pycbc TimeSeries
        tsdata_x:   1d array / a pycbc TimeSeries
                                The plus and cross polarized components of the waveforms.
        delta_t:    float, optional
                                The time step. Overrides the timestep from pycbc TS object if given.
        to_plot:    bool, optional
                                                        True or False. Whether to plot the data or not

        Returns
        -------

        phic:   1d array
                                        The 1d array of the phase of the waveform.
        """

        # Assign the timestep. Real and imaginary parts are assumed to have same timestep.
        # if not delta_t:
        #   try:
        #       delta_t = tsdata_p.delta_t
        #   except AttributeError:
        #       try:
        #           delta_t = tsdata_x.delta_t
        #       except:
        #           message("Input is not a TimeSeries. Please supply gridspacing as delta_t", message_verbosity=0)

        # Assign the timestep. Real and imaginary parts are assumed to have same timestep.
        # Convert the data in numpy arrays

        datap = np.array(tsdata_p)
        datax = np.array(tsdata_x)

        # Calculate the wrapped phase (phi0 -> (0,2Pi))
        phi0 = np.pi + np.arctan2(datax, datap)

        phic = unwrap_phase(phi0)
        # phic = np.unwrap(phi0)
        # Plots.
        # Phase vs time.
        if to_plot:
            timeaxis = np.linspace(0, len(phi0) * delta_t, len(phi0))
            message(len(timeaxis), len(phi0))
            plt.scatter(timeaxis, phi0, s=1)
            plt.title("Phase")
            plt.xlabel("cctk_time")
            plt.ylabel("Phase in radians")
            plt.grid()
            # plt.savefig('../graphs/waveform_phase_{}_q1a0.pdf'.format(name))
            plt.show()

            timeaxis = np.linspace(0, len(phic) * delta_t, len(phic))
            message(len(timeaxis), len(phic))
            # Unwrapped phase vs time
            plt.scatter(timeaxis, phic, s=1)
            plt.title("Phase (unwrpped)")
            plt.xlabel("cctk_time")
            plt.ylabel("Phase in radians")
            plt.grid()
            # plt.savefig('../graphs/waveform_phase_complete_{}_q1a0.pdf'.format(name))
            plt.show()
            # Return a 1d list containing the unwrapped phase
        return phic

    def xtract_camp(tsdata_p, tsdata_x, to_plot=False):
        """Given real and imaginary parts of a complex timeseries,
        extract the amplitude of the complex
        data vector : (tsdata_p + i * tsdata_x)

        Parameters
        ----------
        tsdata_p:   1d array / a pycbc TimeSeries
        tsdata_x:   1d array / a pycbc TimeSeries
                                                        The plus and cross polarized components of the waveforms.
        to_plot:    bool
                                                        True or False. Whether to plot the data or not

        Returns
        -------

        camp:   1d array
                                        The 1d array of extracted amplitudes of the waveform."""

        # Assign the timestep. Real and imaginary parts are assumed to have same timestep.
        # Complex modulous of the data
        camp = np.sqrt(np.array(tsdata_p) ** 2 + np.array(tsdata_x) ** 2)

        if to_plot:
            # Plot amplitude vs time
            plt.plot(camp)
            plt.title("Amplitude vs time")
            plt.xlabel("cctk_time")
            plt.ylabel("Amplitude")
            plt.grid()
            # plt.savefig('../graphs/waveform_phase_complete_{}_q1a0.pdf'.format(name))
            plt.show()
        # Returns the 1d numpy array amplitude
        return camp

    def xtract_camp_phase(tsdata_1, tsdata_2):
        """Wrapper for extracting the amplitude and the phase of the complex vector.

        Parameters
        ----------

        tsdata_1:   1d array or pycbc TimeSeries object
        tsdata_2:   1d array or pycbc TimeSeries object
                    The two input waveforms as timeseries
                    vectors, the plus and cross polarized
                    components.
        delta_t:    float
                                        The timestepping delta_t.

        Returns
        -------

        amplitude:  1d array
                    A list containing complex amplitude
                    (list) and phase (list).
        """

        return xtract_camp(tsdata_1, tsdata_2), xtract_cphase(tsdata_1, tsdata_2)

    def get_waveform_angular_frequency(waveform, delta_t, timeaxis=None, method="FD"):
        """Get the angular frequency of the waveform given
        the complex waveform time step. The phase is
        extracted and is differentiated using one of
        the available methods to compute the angular
        frequency.

        Parameters
        ----------

        waveform:       1d array
                                        The 1d complex array of the input
                                        waveform.

        delta_t:     float
                                 The time step.

        timeaxis:    1d array, optional
                                 The time axis of the waveform.
                                 Recommended especially when the sampling
                                 is non-uniform and Chebyshev method
                                 is chosen.
        method: str
                                The method for computing the derivative.
                                Can be `FD` or `CS`. See below for more
                                information.

        Returns
        -------

        omega:      1d array
                                The real instantaneous frequency of the waveform.

        Notes
        -----

        Available methods

        Chebyshev series (`CS`):    The phase is expanded in
                                                                a Chebyshev series and
                                                                then the differentiated.
        Finite Differencing (`FD`) : A 11 point finite difference
                                                                 scheme is used to differentiate
                                                                 the phase, and is then smoothened
                                                                 using the Savgol filter.

        """
        # Get the real and imaginary parts.
        waveform_p = waveform.real
        waveform_x = waveform.imag
        # Get the phase
        phase = xtract_cphase(waveform_p, waveform_x)
        # Compute the derivative
        if method == "FD":
            # Finite differencing method
            from scipy.signal import savgol_filter
            from waveformtools.differentiate import differentiate5

            # Compute the derivative
            omega_rough = differentiate5(phase, delta_t)
            # Smoothen the derivative
            omega_sm = savgol_filter(omega_rough, 41, polyorder=3)
            # Duplicate the last elements to bring the length to that of input waveform.
            len_omega_sm = len(omega_sm)
            while len_omega_sm != len(phase):
                omega_sm = np.concatenate((omega_sm, np.array([omega_sm[-1]])))
                len_omega_sm = len(omega_sm)

        if method == "CS":
            # Chebyshev spectral method
            from waveformtools.differentiate import Chebyshev_differential

            if not timeaxis:
                # Construct the time axis
                timeaxis = np.arange(0, len(waveform) * delta_t, delta_t)

            omega_sm = Chebyshev_differential(timeaxis, phase, degree=25)

        return omega_sm

    def get_starting_angular_frequency(waveform, delta_t, npoints=400):
        """Get the approximate starting frequency of the
        input data by averaging over the first `npoints` number of points.

        Parameters
        ----------

        waveform:       1d array
                                        The 1d complex array of the input
                                        waveform.

        delta_t:     float
                                 The time step.

        npoints:     int
                                 The number of points to average over.

        Returns
        -------

        omega0: float
                                The approximate starting angular frequency.

        Notes
        -----

        Please suppy a conditioned input waveform that is neatly clipped,
        and not tapered.
        """

        # Get angular frequencies
        omegas = get_waveform_angular_frequency(waveform, delta_t)

        # Compute the starting frequency as the mean of first npoints.
        omega0 = np.mean(omegas[100 : 100 + npoints])

        return omega0

    # Simple overlap. #Error. Add frequency domain overlap computation.
    def olap(data1, data2, psd=1):
        """Calcuate the overlap between two data vectors weighted by the given psd.

        Parameters
        ----------

        data1:  1d array or a pycbc TimeSeries object
        data2 : 1d array or a pycbc RimeSeries object
                                        The input waveforms
        psd:    1d array
                                        The power spectral density to weight.

        Returns
        -------

        overlap:    float
                                                        The overlap divided by the psd.

        """

        if psd == 1:
            data1 = np.array(data1)
            data2 = np.array(data2)
            overlap = np.sum(data1 * data2) / psd

        else:
            1
            # Returns the overlap weighted by the psd if any
        return overlap

    def norm(hdat, psd=1.0):
        """Calculate the norm of a vector.

        Parameters
        ----------

        hdat:   1d array or a pycbc TimeSeries object.
                                        The input waveform.
        psa:    1d array
                                        The noise power spectral density of the inner product.

        Returns
        -------

        norm_f: float
                                        The norm with weighting by the psd.
        """

        hdat = np.array(hdat)

        norm_f = np.sqrt(np.sum(hdat * hdat) / np.array(psd))

        # message("Norm is %f"%normfa)
        return norm_f

    def flatten(nflist):
        """Flatten a (3rd order) list of list of lists.
        i.e. a three tier list [[[],[]], [[],[]] ---> [].
        This is useful e.g. when combining the data from
        the list output of multiple MPI ranks.

        Parameters
        ----------

        nflist: a third tier list
                A list of list of lists (a list of depth three).

        Returns
        -------

        flattened_list: a list
                        The flattened list i.e. a tier one list.
        """

        flattened_list = []

        for item in nflist:
            for sub_item in item:
                flattened_list.append(sub_item)

        message("list length: (%d)" % (len(flattened_list)))
        # Return the 1d flattened list
        return flattened_list

    def startend(data):
        """Identify the start and endpoints of the data.

        Notes
        -----

        The starting and ending index of the non-zero part
        of the data is the identification criterion.
        Requires the data to be exactly zero outside
        a certain domain.

        Parameters
        ----------

        data:   1d array or a pycbc TimeSeries object
                                        The input waveform.

        Returns
        -------

        start_index, end_index: int (2)
                                The pair of indices denoting
                                the start and end points of an array

        """

        try:
            start_index = np.where(np.array(data) != 0)[0][0]
        except BaseException:
            message(colored("Warning! Start index not found!!", "red"), message_verbosity=1)
            start_index = 0

        try:
            end_index = np.where(np.array(data) != 0)[0][-1] + 1
        except BaseException:
            message(colored("Warning! End index not found!!", "red"), message_verbosity=1)
            end_index = 0
        return start_index, end_index

    def apxstartend(data, tol=1e-5):
        """Identify the Approximate start and endpoints of the data.

        Notes
        -----

        The starting and ending index of the peak
        tol (default 1e-5)  part of the data is
        the identification criterion. Requires
        the data to fall off to tol*peak absolute
        value outside a certain range.

        Parameters
        ----------

        data:   1d array or a pycbc TimeSeries object
                The input waveform whose start and
                end points need to be identified based
                on a given tolerance value.
        tol:    float
                The tolerance value below which
                the signal is assumed to be absent.

        Returns
        --------

        loc_pair:   int (2)
                    The pair of indices denoting
                    the start and end points of an array
        """

        data = np.array(data)
        locs = np.where(data > np.amax(data) * tol)[0]

        loc_pair = locs[0], locs[-1] + 1

        # Return the beginning and ending indices
        return loc_pair

    def addzeros(data, zeros):
        """Append zeros to an array without tapering.

        Parameters
        ----------

        data    :   1d array or a pycbc TimeSeries
                The waveform data as list or numpy array or pycbc timeseries.

        zeros   :   int
                The number of zeros to be added.

        Returns
        -------

        data with `zeros` number of zeros concatenated at the end as numpy 1d array :   trial
        """

        return np.transpose(np.concatenate((np.transpose(np.array(data)), np.transpose(np.zeros([zeros])))))

    def removezeros(data, delta_t):
        """Remove zeros from the input waveform from either sides.
        Similar to startend but return the truncated array.

        Parameters
        ----------

        data:   1d array or a pycbc TimeSeries
                                        The input waveform.
        delta_t:    float, optional
                                        The time stepping.

        Returns
        -------

        short_ts:   a list
                    A list containing  waveforms with zeros removed on either sides,
                    the start and end indices in the format [short_ts, [start_index, end_index]]

        """

        # Assign the timestep. Real and imaginary parts are assumed to have same
        # timestep.
        if not delta_t:
            try:
                delta_t = data.delta_t
            except BaseException:
                message("Input is not a TimeSeries." " Please supply gridspacing as delta_t", message_verbosity=0)

        starti, endi = startend(data)
        ret_data = pycbc.types.timeseries.TimeSeries(np.array(data)[starti:endi], delta_t)

        short_ts = [ret_data, [starti, endi]]

        return short_ts

    def shorten(tsdata, start, end, delta_t=None):
        """Shorten an array given the start and end points.

        Parameters
        ----------

        tsdata: 1d array or a pycbc TimeSeries object
                                                        The waveform data.
        start:  int
                                        The start index of the data.

        end:    int
                                        The end index of the data.
        delta_t:    float, optional
                        The time stepping.

        Returns
        -------

        short_ts:   a pycbc TimeSeries object
                                                        The shortened data, clipped before start and after end.
        """
        # Assign the timestep.
        # Real and imaginary parts
        # are assumed to have same

        # timestep.
        if not delta_t:
            try:
                delta_t = tsdata.delta_t
            except BaseException:
                message("Input is not a TimeSeries." "Please supply gridspacing as delta_t", message_verbosity=0)

        short_ts = pycbc.types.timeseries.TimeSeries(np.array(tsdata)[start:end], delta_t)

        return short_ts

    def taper_tanh(waveform, time_axis=None, delta_t=None, duration=10, sides="both"):
        """
        Taper a waveform with a :math:`tanh` function
        at either ends

        Parameters
        ----------
        waveform:    1d array
                     A 1d array of waveform data.
        delta_t:     float, optional
                     The time stepping `delta_t`.
                     Optional if `time_axis` is given.
        percent:     int, optional
                     The percent of data to taper. This
                     is equally distributed on either
                     sides of the array. Defaults to 10.
        sides:       str
                     A string indicating which sides to taper.
                     `beg` tapers the beginning, `end` tapers the end
                    and `both` tapers both the ends.
        Returns
        -------
        time_axis, waveform:    1d array
                                The timeaxis and the waveform data
                                of the tapered waveform.
        """

        data_len = len(waveform)

        if delta_t is None:
            try:
                delta_t = time_axis[1] - time_axis[0]
            except Exception as excep:
                delta_t = 1
                message("Assuming unity for time step", excep)

        #    delta_t = 1
        if time_axis is None:
            # Try to construct using `delta_t`
            try:
                time_axis = np.arange(0, data_len * delta_t, delta_t)
                # print('time axis', time_axis)
            except Exception as excep:
                message("Please suppy the time axis or delta_t!", excep)

        nearest_lower_power = int(np.log(data_len) / np.log(2))

        # print('nearest lower power', nearest_lower_power)
        # Change in length of the data.
        data_delta_len = np.power(2, nearest_lower_power + 2) - data_len
        # print('data delta len', data_delta_len)
        # nend_points = int((duration/2)/delta_t) + int(data_delta_len/2)
        nend_points = int(data_delta_len / 2)
        nstart_points = data_delta_len - nend_points

        # print('N startend points', nend_points, nstart_points)
        # print('New time axis', new_time_axis)
        tfinal = data_len * delta_t

        # start_axis = np.linspace(-1, 1, nstart_points)
        # end_axis   = np.linspace(1, -1, nend_points)

        # norm_time_axis = time_axis/max(time_axis)

        # n_delta_t = delta_t/data_len

        # from scipy.interpolate import interpolate
        waveform_widened = np.concatenate((np.zeros([nstart_points]), waveform, np.zeros([nend_points - 1])))
        new_time_axis = np.linspace(
            time_axis[0] - nstart_points * delta_t, time_axis[-1] + nend_points * delta_t, len(waveform_widened)
        )

        start_win = (np.tanh(3 * (new_time_axis - duration / 2) / (duration / 2)) + 1) / 2
        end_win = (np.tanh(3 * (-new_time_axis + (tfinal - duration / 2)) / (duration / 2)) + 1) / 2

        # plt.scatter(new_time_axis, waveform_widened, s=1)
        # plt.show()

        if sides == "both":
            tapered_waveform = start_win * end_win * waveform_widened
        elif sides == "beg":
            tapered_waveform = start_win * waveform_widened
        elif sides == "end":
            tapered_waveform = end_win * waveform_widened
        else:
            message("Please specify valid sides argument. Sides can be beg, end or both.")

        # plt.scatter(new_time_axis, tapered_waveform, s=1)
        # plt.show()

        return new_time_axis, tapered_waveform

    def taper(data, delta_t=1, zeros=150):
        """A method to taper and append additional zeros
        at either ends, using the `taper` function of
        the pycbc TimeSeries object.

        Parameters
        ----------

        data:   1d array or a pycbc TimeSeries
                The waveform data as list or numpy
                array or pycbc timeseries.
        delta_t:    float
                                The timestepping.
        zeros:  int
                        The number of zeros to be added.

        Returns
        -------

        tapered_data:   1d array or a pycbc TimeSeries object
                        The waveform data tapered and zero padded.
                        Returns a pycbc TimeSeries object.

        Notes
        -----
        See `taper_timeseries` from pycbc.waveform.utils for more details."""

        import pycbc

        # Check if data is pycbc timeseries:
        if not isinstance(data, pycbc.types.timeseries.TimeSeries):
            # flag = 1
            # Convert to numpy array
            data = np.array(data)
            # First taper both sides of the data i.e. the start and end of the data.
            # Convert to pycbc TimeSeries
            data = pycbc.types.timeseries.TimeSeries(data, delta_t)
        else:
            delta_t = data.delta_t

        # Taper the timeseries
        from pycbc.waveform.utils import taper_timeseries

        tapered_data = taper_timeseries(data, tapermethod="TAPER_STARTEND")

        # Append the zeros
        tapered_data = np.array(tapered_data)

        # Pad ends with extra zeros
        zeros = np.zeros([zeros])
        # Prepend with z zeros
        tapered_data = np.transpose(np.concatenate((np.transpose(zeros), np.transpose(tapered_data))))
        # Append with extra zeros
        tapered_data = np.transpose(np.concatenate((np.transpose(tapered_data), np.transpose(zeros))))

        # Convert back to timeseries if the input was a time series.
        tapered_data = pycbc.types.timeseries.TimeSeries(tapered_data, delta_t)

        # Return the timeseries
        return tapered_data

    def low_cut_filter(utilde, freqs, order=2, omega0=0.03):
        """Apply low frequency cut filter using a butterworth filter.

        Parameters
        ----------
        utilde:    1d array
                   The frequency domain data.
        freqs:     1d array
                   The frequencies.
        order:     int
                   The order of the butterworth filter.
        omega0:    float
                   The cutoff frequency of the butterworth filter.

        Returns
        -------
        utilde_lc:    1d array
                      The filtered data.
        """

        from scipy import signal
        from scipy.interpolate import interp1d

        # import matplotlib.pyplot as plt

        b, a = signal.butter(order, omega0, "high", analog=True)

        w, h = signal.freqs(b, a)

        # Make negative axis and data.
        filter_freqs = np.concatenate((-w[::-1], w))
        filter_coeffs = np.concatenate((np.conjugate(h[::-1]), h))

        # Resample the filter at data freqs.
        filter_int = interp1d(filter_freqs, filter_coeffs)
        filter_resam = filter_int(freqs)

        filtered_signal = utilde * filter_resam

        return filtered_signal

    def center(wvp, wvc=None, delta_t=None):
        """Center a waveform (wvp, wvc) at the peak
        of the complex modulous sqrt(wvp**2 + wvc**2).

        Parameters
        ----------

        wvp:    1d array or a pycbc TimeSeries object
        wvc:    1d array or a pycbc TimeSeries object
                The one/two components of the waveforms
                as 1d list or numpy arrays or pycbc timeseries.
        delta_t:    float
                    The timestepping delta_t.

        Returns
        -------

        centered_wf:    a pycbc TimeSeries objet
                        The two 1d centered waveform(s) as individual pycbc timeseries.

        Procedure
        ---------

        1. Findout if both polarizations are supplied. If not assume cross pol is plus.
        2. Find the absolute magnitde location in the array.
        3. Construct the time limits using this information.
        3. Construct timeseries with epoch as first index of the array.

        """
        # Flag to find out if both polarizations are supplied or not.
        flag = 0
        # If only one waveform is provided, assume cross pol = plus pol.
        if not wvc:
            flag = 1
            wvc = wvp
        # Assign the timestep. Real and imaginary parts are assumed to have same
        # timestep.
        if not delta_t:
            try:
                delta_t = wvp.delta_t
            except AttributeError:
                try:
                    delta_t = wvc.delta_t
                except BaseException:
                    message("Input is not a TimeSeries." "Please supply gridspacing as delta_t", message_verbosity=0)

        datap = np.array(wvp)
        datac = np.array(wvc)

        # Assign the complex amplitude
        amp = np.power(datap, 2) + np.power(datac, 2)
        # Find the location of the max amplitude
        ind = np.where(amp == np.max(amp))[0][0]
        # Calculate the epoch
        tlim = [-ind * delta_t, (len(datap) - ind) * delta_t]
        # Returns the centered wvp and wvc if wvc was provided else returns just
        # the former.

        if flag == 1:
            centered_wf = pycbc.types.timeseries.TimeSeries(datap, delta_t, epoch=tlim[0])
        else:
            centered_wf = (
                pycbc.types.timeseries.TimeSeries(datap, delta_t, epoch=tlim[0]),
                pycbc.types.timeseries.TimeSeries(datac, delta_t, epoch=tlim[0]),
            )

        return centered_wf

    def get_centered_taxis(time_ax, amps):
        """Get the time axis of the waveform centered
            at its maximum absolute amplitude.

        Parameters
        ----------

        time_ax:    1d array
                                The 1d array containg
                                the original (uncentered)time axis of the wveform.

        amps    :   1d array
                                The amplitude of the waveform.

        Returns
        -------

        time_centered:  1d array
                        The centered time axis of the waveform.
                        The maximum amplitude timestap will be at :math:`t=0`.
        """

        # Get the maximum amplitude
        amps_max = np.argmax(abs(amps))
        # Get the time stamp of the max amp location.
        time_max = time_ax[amps_max]
        # Center the time axis.
        time_cen = time_ax - time_max

        return time_cen

    def plot(xdata, func_x, save="no"):
        """A Basic plotting function.

        Parameters
        ----------

        xdata:  1d array
                        The x axis of the function,
        func_x: 1d array
                                        The y axis of the function f(x),
        save:   bool.
                                        True or False. Whether the plot should be saved or not.

        Returns
        -------

        1:  (int)
        plots:  figures to stdout and disk
                        Displays the plot, and Saves with the filename provided.
        """

        plt.plot(np.array(xdata), np.array(func_x))
        plt.title("f(x) vs x")
        plt.grid(which="both", axis="both")
        plt.xlabel("xdata")
        plt.ylabel("f(x)")
        if save != "no":
            plt.savefig(save + ".pdf")
        plt.show()
        return 1

    # Custom coalign function

    def coalignwfs(tsdata1, tsdata2, delta_t=None):
        """Coalign two timeseries.
            Wrapper and modification around pycbc functions
            with some additional functionalities.

        Parameters
        ----------

        tsdata1:    list, 1d array or a pycbc TimeSeries
        tsdata2:    list, 1d array or a pycbc TimeSeries
                    The two data vectors as 1d lists or
                    numpy arrays or pycbc TimeSeries
        delta_t:    float
                    The time stepping.

        Returns
        -------

        ctsdata1:   a pycbc TimeSeries
        tsdata2:    a pycbc TimeSeries
                    A pair of pycbc TimeSeries objects;
                    the aligned first waveform and the second.

        Procedure
        ---------

        1. Adjust length of either waveforms if needed
        2. Compute The complex SNR.
        3. Shift and roll the first.

        """

        # Lengths of the two input timeseries
        # len1 = len(tsdata1)
        # len2 = len(tsdata2)

        # Add zeros at the end of waveform 1 without tapering if len2>len1
        # if len2>len1:
        #                tsdata1,lflag = lengtheq(tsdata2,tsdata1,tsdata1.delta_t)
        # Add zeros at the end of waveform 2 without tapering if len1>len2
        # elif len1>len2:
        #                tsdata2,lflag = lengtheq(tsdata2,tsdata1,tsdata1.delta_t)

        # Assign the timestep. Real and imaginary parts are assumed to have same
        # timestep.

        if not delta_t:
            try:
                delta_t = tsdata1.delta_t
            except AttributeError:
                try:
                    delta_t = tsdata2.delta_t
                except BaseException:
                    message("Input is not a TimeSeries." "Please supply gridspacing as delta_t", message_verbosity=0)

        tsdata1, tsdata2, _ = lengtheq(tsdata1, tsdata2, delta_t, is_ts=True)

        # Calculate complex SNR using pycbc function. Note: This complex SNR is
        # actually the complex SNR * norm of the timeseries.
        csnr = pycbc.filter.matchedfilter.matched_filter(tsdata1, tsdata2)
        # Find the absolute value of the complex SNR timeseries
        acsnr = np.array(np.abs(csnr))

        # message(acsnr,np.max(acsnr))
        # Find the location of the maximum element in acsnr
        maxloc = (np.where(acsnr == np.max(acsnr)))[0][0]
        message("Max location is %s, match is %s" % (maxloc, np.max(acsnr)))

        # Shift the waveform 1 in time using maxloc
        tsdata1 = roll(tsdata1, maxloc, is_ts=True)

        # tsdata1 = shiftmatched(tsdata1, maxloc, delta_t, is_ts=True)
        # Phase shift ( rotate) the waveform 1 by multipying
        # the frequency series of waveform 1 with the phase of the max element in acsnr
        # Calculate the rotation (as phase of the max complex modulous element in
        # acsnr
        rotation = csnr[maxloc] / np.absolute(csnr[maxloc])
        # Rotate and take the inverse Fourier transform
        ctsdata1 = (rotation * tsdata1.to_frequencyseries()).to_timeseries()
        # Return the time and phase shifted waveform 1 to coalign with 2 and waveform 2.
        # Note that the max modulous element of acsnr is only used to compute the time
        # shift and is not used to normalize the
        # waveforms. This therefore returns waveforms with their original
        # normalization.

        return ctsdata1, tsdata2

    def coalignwfs2(tsdata1, tsdata2, delta_t=None):
        """Coalign two waveforms function 2.

        Parameters
        ----------

        tsdata1:    1d array or a pycbc TimeSeries
        tsdata2:    1d array or a pycbc TimeSeries
                    two data vectors as 1d lists or
                    numpy arrays or pycbc timeseries.

        Returns
        --------

        aligned_waveforms:   list
                            The aligned waveforms in the format
                            [aligned_wf1,
                            aligned_wf2,
                            [norm1, norm2, location of maximum]].

        Notes
        -----

        Procedure:

        1. Adjust length of either waveforms if needed

        2. Normalize the two timeseries

        3. Compute The complex SNR

        4. Shift and roll the first

        5. Returns normalized waveforms

        See coalignwfs for description. This algorithm does not use
        the builtin coalign function from pycbc. This involves normalization
        of the data vectors explicitly and identifiies the timeshift by computing
        the complex SNR and finding the maximum element.
        """
        # Lengths of the two input timeseries
        len1 = len(tsdata1)
        len2 = len(tsdata2)

        if not delta_t:
            try:
                delta_t = tsdata1.delta_t
            except AttributeError:
                try:
                    delta_t = tsdata2.delta_t
                except Exception as excep:
                    message("Please input delta_t or a valid TimeSeries!", excep)

        tsdata1, tsdata2, _ = lengtheq(tsdata2, tsdata1, delta_t, is_ts=True)

        # Find startend.
        if len1 == len2:
            try:
                start, end = startend(np.array(tsdata1))
            except BaseException:
                start, end = apxstartend(np.array(tsdata1))

        # Add zeros at the end of waveform 1 without tapering if len2>len1
        if len2 > len1:
            #       tsdata1,lflag = lengtheq(tsdata2,tsdata1,tsdata1.delta_t)
            try:
                start, end = startend(np.array(tsdata1))
            except BaseException:
                start, end = apxstartend(np.array(tsdata1))

        # Add zeros at the end of waveform 2 without tapering if len1>len2
        elif len1 > len2:
            # tsdata2,lflag = lengtheq(tsdata2,tsdata1,tsdata1.delta_t)
            try:
                start, end = startend(np.array(tsdata2))
            except BaseException:
                start, end = apxstartend(np.array(tsdata2))

        # match,shift=pycbc.filter.matchedfilter.match(tsdata1,tsdata2)

        # Normalize the waveforms
        norm1 = norm(np.array(tsdata1[start:end]))
        norm2 = norm(np.array(tsdata2[start:end]))

        tsdata1_cropped = pycbc.types.timeseries.TimeSeries(np.array(tsdata1[start:end]) / norm1, delta_t)
        tsdata2_cropped = pycbc.types.timeseries.TimeSeries(np.array(tsdata2[start:end]) / norm2, delta_t)

        max_match, max_shift = pycbc.filter.matchedfilter.match(tsdata1_cropped, tsdata2_cropped)

        tsdata1 = pycbc.types.timeseries.TimeSeries(np.array(tsdata1) / norm1, delta_t)
        tsdata2 = pycbc.types.timeseries.TimeSeries(np.array(tsdata2) / norm2, delta_t)

        # Calculate complex SNR using pycbc function. Note: This complex SNR is
        # actually the complex SNR * norm of the timeseries.
        csnr = pycbc.filter.matchedfilter.matched_filter(tsdata1, tsdata2)

        # Find the absolute value of the complex SNR timeseries
        acsnr = np.array(np.abs(csnr))
        # message(acsnr,np.max(acsnr))

        # Find the location of the maximum element in acsnr
        maxloc = (np.where(acsnr == np.max(acsnr)))[0][0]
        mmatch = np.amax(acsnr)
        message(f"Max location is {maxloc}," f"match is {mmatch}" f"max shift is {max_shift}")

        # Shift the waveform 1 in time using maxloc
        tsdata1 = shiftmatched(tsdata1, maxloc, delta_t, is_ts=True)

        # Phase shift ( rotate) the waveform 1 by multipying
        # the frequency series of waveform 1 with the phase
        # of the max element in acsnr
        # Calculate the rotation (as phase of the max complex
        # modulous element in
        # acsnr
        rotation = csnr[maxloc] / np.absolute(csnr[maxloc])

        # Rotate and take the inverse Fourier transform
        ctsdata1 = (rotation * tsdata1.to_frequencyseries()).to_timeseries()

        # Recenter waveform 0 and assign the timeaxis of waveform 0 to waveform1
        ctsdata1, dummy = center(ctsdata1, ctsdata1)
        tsdata2 = pycbc.types.timeseries.TimeSeries(np.array(tsdata2), tsdata2.delta_t, epoch=ctsdata1.sample_times[0])

        # Return the normalized, time and phase shifted waveform 1 to coalign with
        # 2 and waveform 2.
        aligned_waveforms = {"wf1": ctsdata1, "wf2": tsdata2, "norms": [norm1, norm2], "shift": maxloc, "match": max_match}

        return aligned_waveforms

    def resample_wfs(both_time_axes, both_waveforms, delta_t="auto", Plot=False):
        """Resample the waveform pairs.

        Parameters
        ----------
        both_time_axes : list
                         A list containing two 1d arrays representing the time axes.
        both_waveforms : list
                         A list containing two 1d arrays respresenting the waveforms.
        delta_t        : string, float
                         The time step to resample at. Auto uses the finest of the two available.
                         A float value can be provided by the user as well.

        Returns
        -------
        both_time_axes_resam : 1d array
                               A 1d array representing the resampled time axes.
        both_waveforms_resam : list
                               A list containing two 1d arrays respresenting the resampled waveforms.
        """

        # from waveformtools.waveformtools import lengtheq

        # from scipy.interpolate import interp1d

        waveform1, waveform2 = both_waveforms
        time_axis1, time_axis2 = both_time_axes

        min_t = max(min(time_axis1), min(time_axis2))
        max_t = min(max(time_axis1), max(time_axis2))

        print("Taxis limits")
        print(f"WF1 tmin {min(time_axis1)} tmax {max(time_axis1)}")
        print(f"WF2 tmin {min(time_axis2)} tmax {max(time_axis2)}")

        delta_t1 = sorted(time_axis1)[1] - sorted(time_axis1)[0]
        delta_t2 = sorted(time_axis2)[1] - sorted(time_axis2)[0]

        if not isinstance(delta_t, float):
            if delta_t == "auto":
                delta_t = min(delta_t1, delta_t2)
            elif delta_t == "A":
                delta_t = delta_t1
            elif delta_t == "B":
                delta_t = delta_t2

        # time_axis = shear_1.time_axis
        # waveform1, waveform2, flag = lengtheq(waveform1, waveform2, delta_t=delta_t)

        new_time_axis = np.arange(min_t, max_t, delta_t)

        wf1_amp, wf1_phase = xtract_camp_phase(waveform1.real, waveform1.imag)
        wf2_amp, wf2_phase = xtract_camp_phase(waveform2.real, waveform2.imag)

        # wf1_amp_int_fun     = interp1d(time_axis1, wf1_amp, kind='cubic')
        # wf1_phase_int_fun   = interp1d(time_axis1, wf1_phase, kind='cubic')

        # wf1_amp_resam       = wf1_amp_int_fun(new_time_axis)
        # wf1_phase_resam     = wf1_phase_int_fun(new_time_axis)

        wf1_amp_resam = interp_resam_wfs(wf1_amp, time_axis1, new_time_axis)
        wf1_phase_resam = interp_resam_wfs(wf1_phase, time_axis1, new_time_axis)

        wf1_resam = wf1_amp_resam * np.exp(1j * wf1_phase_resam)

        wf2_amp_resam = interp_resam_wfs(wf2_amp, time_axis2, new_time_axis)
        wf2_phase_resam = interp_resam_wfs(wf2_phase, time_axis2, new_time_axis)

        # wf2_amp_resam       = wf2_amp_int_fun(new_time_axis)
        # wf2_phase_resam     = wf2_phase_int_fun(new_time_axis)

        wf2_resam = wf2_amp_resam * np.exp(1j * wf2_phase_resam)

        return new_time_axis, wf1_resam, wf2_resam

    def match_wfs(all_time_axes, all_waveforms, delta_t="auto"):
        """Match two waveforms and return the time shift,
        phase shift, normalized waveforms and match coefficient.

        Parameters
        ----------
        time_axes: list
                A list containing the time axes
                of the two waveforms

        waveforms: list
                    A list of two waveforms.
                    Each is a 1d array.
        delta_t: float, string, optional
                 The time step of the resampled arrays.
                 Can be A, B, auto or any float value.

        Returns
        -------
        match_details: dict
                        A dictionary containing the
                        i). match coeffient
                        ii). time_shift
                        iii). phase shift in radians
                        iv). normalized, resampled, waveforms and their
                             time-axes.

        Note
        ----
        The shifts give by how much the first waveform has to be shifted to match with the second.

        """

        time_axis1, time_axis2 = all_time_axes

        delta_t_A = sorted(time_axis1)[1] - sorted(time_axis1)[0]
        delta_t_B = sorted(time_axis2)[1] - sorted(time_axis2)[0]

        if isinstance(delta_t, str):
            if delta_t == "auto":
                from scipy.stats import mode

                # tsorted = sorted(time_axis1)
                # delta_t = tsorted[0] - tsorted[1]

                delta_t = mode(np.diff(sorted(time_axis1)))[0][0]

                # delta_t = min(delta_t_A, delta_t_B)
            elif delta_t == "A":
                delta_t = delta_t_A
            elif delta_t == "B":
                delta_t = delta_t_B
            else:
                raise ValueError(f"Did not understand speification for delta_t {delta_t}")

        # print(type(time_axis1), type(time_axis2))
        # print(time_axis1-time_axis2)
        # print(time_axis1==time_axis2)

        # Don't interpolate if the time axis are identical
        Interp = True

        if len(time_axis1) == len(time_axis2):
            if (time_axis1 == time_axis2).all():
                Interp = False
                time_axis = time_axis1
                wf1, wf2 = all_waveforms
                message("waveforms have a common time axis")

                # from scipy.stat import mode
                # tsorted = sorted(time_axis1)
                # delta_t = tsorted[0] - tsorted[1]

                # delta_t = mode(np.diff(time_axis1))

        if Interp is True:
            message("Interpolating time axis")
            time_axis, wf1, wf2 = resample_wfs(all_time_axes, all_waveforms, delta_t)
            # print(time_axis, wf1, wf2)

        from waveformtools.transforms import compute_fft, compute_ifft

        # The Fspace waveforms
        faxis, wf1_tilde = compute_fft(wf1, delta_t)
        _, wf2_tilde = compute_fft(wf2, delta_t)
        delta_f = faxis[1] - faxis[0]
        # Compute the Fspace complex SNR
        csnr_tilde = wf1_tilde * np.conjugate(wf2_tilde)
        # Tspace CSNR
        _, csnr = compute_ifft(csnr_tilde, delta_f)
        # T space CSNR amp and phase
        Acsnr = np.absolute(csnr)

        # Compute shift quantities for waveform 2 against 1
        # Time shift
        Tshift_rec_index = np.argmax(Acsnr)
        # max_snr = np.amax(Acsnr)

        Tshift_rec = (len(time_axis) - Tshift_rec_index) * delta_t
        # Phase shift
        Pshift_rec = csnr[Tshift_rec_index] / np.absolute(csnr[Tshift_rec_index])
        Pshift_rec_rad = np.log(Pshift_rec) / (1j)

        print("-----------------------------------\n Shift information for waveform 2 against 1 \n")
        print(f"Recovered Time shift: {Tshift_rec}")
        print(f"Recovered Phase shift: {Pshift_rec}, {Pshift_rec_rad} in radians")
        print("-----------------------------------")

        # Apply the time shift to the second waveform

        if Tshift_rec_index != 0:
            wf2_Trec = roll(wf2, Tshift_rec_index)
        else:
            wf2_Trec = wf2

        if Pshift_rec != 1:
            _, wf2_Trec_tilde = compute_fft(wf2_Trec, delta_t)
            Pshift2_rad = 2 * np.pi - Pshift_rec_rad
            Pshift2_fac = np.exp(1j * Pshift2_rad)
            wf2_TPrec_tilde = wf2_Trec_tilde * Pshift2_fac

            _, wf2_TPrec = compute_ifft(wf2_TPrec_tilde, delta_f)

        else:
            # Check error!
            wf2_TPrec = wf2_Trec

        norm1 = np.sqrt(np.sum(wf1 * np.conjugate(wf1)))
        norm2 = np.sqrt(np.sum(wf2_TPrec * np.conjugate(wf2_TPrec)))

        waveform1_aligned = wf1 / norm1
        waveform2_aligned = wf2_TPrec / norm2

        match_score = np.sum(waveform1_aligned * np.conjugate(waveform2_aligned))  # max_snr/(norm1*norm2)

        match_details = {
            "match_score": match_score,
            "time_shift": Tshift_rec,
            "phase_shift": Pshift_rec_rad,
            "time": time_axis,
            "aligned_waveforms": [waveform1_aligned, waveform2_aligned],
        }

        return match_details

    def match_wfs_pycbc(all_time_axes, all_waveforms):
        """Match two waveforms using pycbc subroutines and return the time shift,
        phase shift, normalized waveforms and match coefficient.

        Parameters
        ----------
        time_axes: list
                A list containing the time axes
                of the two waveforms

        waveforms: list
                    A list of two waveforms.
                    Each is a 1d array.

        change: int
                Which waveform to change, 1 or 2.

        Returns
        -------
        match_details: dict
                        A dictionary containing the
                        i). match coeffient
                        ii). time_shift
                        iii). phase shift
                        iv). normalized waveforms and their
                             time-axes.
        """

        # Step 1: resample
        from scipy.interpolate import interp1d

        # from waveformtools.waveformtools import match_wfs
        # from waveformtools.waveformtools import lengtheq

        waveform1, waveform2 = all_waveforms
        time_axis1, time_axis2 = all_time_axes

        print("Taxis limits")
        print(f"Shear tmin {min(time_axis1)} tmax {max(time_axis1)}")
        print(f"News tmin {min(time_axis2)} tmax {max(time_axis2)}")

        delta_t_1 = time_axis1[1] - time_axis1[0]
        delta_t = time_axis2[1] - time_axis2[0]

        wf1_amp, wf1_phase = xtract_camp_phase(waveform1.real, waveform1.imag)
        wf2_amp, wf2_phase = xtract_camp_phase(waveform2.real, waveform2.imag)

        wf1_amp_int_fun = interp1d(time_axis1, wf1_amp)
        wf1_phase_int_fun = interp1d(time_axis1, wf1_phase)

        wf1_amp_resam = wf1_amp_int_fun(time_axis2)
        wf1_phase_resam = wf1_phase_int_fun(time_axis2)

        delta_phase = wf1_phase_resam - wf2_phase

        from waveformtools.waveformtools import roll

        corrs = []
        for index in range(len(time_axis2)):
            rwf2 = roll(wf2_amp, index)

            corrs.append(np.dot(rwf2, wf1_amp_resam))

        # maxloc = np.argmax(corrs)
        shift = np.argmax(np.array(corrs))
        print(f"The shift units is {shift}")

        # ii). apply the time shift to the second waveform

        if shift != 0:
            # wf2_amp_shifted = roll(wf2_amp, shift)
            # wf2_phase_shifted = roll(wf2_phase, shift)
            wf2_shifted = roll(waveform2, shift)
        else:
            # wf2_amp_shifted = wf2_amp
            # wf2_phase_shifted = wf2_phase
            # wf2_shifted =  wf2_amp_shifted * np.exp(1j*wf2_phase_shifted)
            wf2_shifted = waveform2
            # waveform2

        mid = int(len(time_axis2) / 2)
        phase_shift = np.mean(delta_phase[mid - 100 : mid + 100])

        from waveformtools.transforms import compute_fft, compute_ifft

        faxis0, wf1_tilde = compute_fft(waveform1, delta_t_1)

        delta_f = faxis0[1] - faxis0[0]

        wf1_tilde_phase_shifted = wf1_tilde * np.exp(1j * phase_shift)
        taxis0, wf1_aligned = compute_ifft(wf1_tilde_phase_shifted, delta_f)

        maxloc = np.argmax(np.absolute(wf1_aligned))
        taxis0 = taxis0 - taxis0[maxloc]

        start_time = min(time_axis2)
        end_time = max(time_axis2)

        start_ind = int((start_time - taxis0[0]) / delta_t)
        end_ind = int((end_time - taxis0[0]) / delta_t)

        wf1_aligned_cropped = wf1_aligned[start_ind:end_ind]

        aligned_time_axis = taxis0[start_ind:end_ind]

        norm1 = np.sum(wf1_aligned_cropped * np.conjugate(wf1_aligned_cropped))
        norm2 = np.sum(waveform2 * np.conjugate(waveform2))

        mlen = min(len(wf1_aligned_cropped), len(wf2_shifted))

        waveform1_aligned = wf1_aligned_cropped[:mlen] / norm1
        waveform2_aligned = wf2_shifted[:mlen] / norm2

        aligned_time_axis = aligned_time_axis[:mlen]
        match_score = np.dot(wf1_aligned_cropped[:mlen], np.conjugate(wf2_shifted[:mlen])) / (norm1 * norm2)

        match_details = {
            "match_score": match_score,
            "time_shift": shift * delta_t,
            "phase_shift": phase_shift,
            "time": aligned_time_axis,
            "aligned_waveforms": [waveform1_aligned, waveform2_aligned],
        }

        return match_details

    def simplematch_wfs_old(waveforms, delta_t=None):
        """Simple match the given waveforms. Does not clip the waveforms at either ends.

        Procedure
        ---------

        1. For each pair of waveforms as a list:
                                    a. Findout if delt has been specified.
                                    b. Findout if the object has attribute delta_t
                                    to discern whether it is a pycbc timeseries (
                                    not exactly. ). If not then exit.
                                    c. Equalize the lengths.
                                    d. Compute the match score and shift using
                                    the pycbc shift function.
                                    e. Findout the start and the end of
                                    the waveform using handles.startend.
                                    f. Reconstruct normalized and clipped
                                    pycbc.timeseries of the waveforms.
                                    g. Confirm the equalization of the lengths of the waveoforms.
                                    h. Append the match details to an
                                    array [ waveform list, [ match score, shift, start_index, end_index]]
        2. Retun the match details for all the waveforms.

        Parameters
        ----------

        waveforms:  list
                                A list of pairs [waveform A, waveform B] of waveforms.
        delta_t:    float, optional
                                The time stepping.

        Notes
        -----

        The time stepping delta_t is the same for each pair of waveforms in the list.

        Returns
        -------

        match:  list
                A list of dicts [{ Aligned waveforms} ,
                {match score (float), shift (number)}]
                containing the match information for all
                the input waveform pairs.
        """

        match = []
        # Iterate over (signal,template) pairs in waveforms
        for waveformdat in waveforms:
            # Carryout the match
            if not delta_t:
                try:
                    delta_t = waveformdat[0].delta_t
                except BaseException:
                    message("Waveform is not a pycbc TimeSeries." "Please provide the gridspacing delt")
                    sys.exit(0)
            # Match procedure
            # signaldat = lengtheq(waveformdat[0], waveformdat[1], delta_t)
            waveform1, waveform2, _ = lengtheq(waveformdat[0], waveformdat[1], delta_t)

            # waveform1 = signaldat[0]
            # waveform2 = signaldat[1]

            # alignedwvs = pycbc.waveform.utils.coalign_waveforms(signaldat,hpa)
            # Compute the match to calculate match and shift.
            # Note: The match function from pycbc returns the match of the
            # normalized templates
            (match_score, shift) = pycbc.filter.matchedfilter.match(waveform1, waveform2)

            # Coalign the waveforms using pycbc coalign.
            waveform1, waveform2 = pycbc.waveform.utils.coalign_waveforms(waveform1, waveform2)

            # Normalize the waveforms

            waveform1 = waveform1 / norm(waveform1)
            waveform2 = waveform2 / norm(waveform2)

            try:
                (match_score, shift) = pycbc.filter.matchedfilter.match(waveform1, waveform2)
            except BaseException:
                message("Final match couldn't be found!")
                match_score = None
                shift = None

            match.append({"Waveforms": [waveform1, waveform2], "Match score": match_score, "Shift": shift})
        return match

    def pmmatch_wfs(waveforms, offset=25, crop=None):
        """
        Match function for post merger waveforms.

        Procedure
        ---------

        1. Equalize the lengths
        2. Crop the waveforms if necessary.
        3. Normalize to their respective maximum amplitudes.
        4. Align the waveforms in phase.
        5. Compute the match score.

        Parameters
        ----------

        waveforms:  a list of pairs
                    The pairs of waveforms to match
                    in the format
                    [[wf1_pair1, wf2_pair2], [wf1_pair_2, wf2_pair2], ...].
        offset: int
                Number of indices to shift the data.
        crop:   string
                A string to decide how to crop the waveforms.
                The available Options are 1. `signal` 2. `template` 3. `both`.

        Returns
        -------

        matchdet:   a list of dicts
                    A list of dictionaries.
                    Each contains
                    1. the waveform pair,
                    2. the match score,
                    3. the shift index. to maximize the match.

        """
        matchdet = []
        for waveformdat in waveforms:
            signal, template = waveformdat

            # message(type(signal), type(template))

            # message(type(signal), type(template), len(signal), len(template))

            signal, template, _ = lengtheq(signal, template)

            # message(type(signal), type(template))

            # Crop the template
            if crop == "both":
                signal = signal[np.argmax(np.array(signal)) + offset :]
                template = template[np.argmax(np.array(template)) + offset :]

            if crop == "signal":
                signal = signal[np.argmax(np.array(signal)) + offset :]

            if crop == "template":
                template = template[np.argmax(np.array(template)) + offset :]

            # message(type(signal), type(template))
            # message(np.amax(np.array(template_plus)), np.amax(np.array(signal_plus)))

            # Normalize the waveforms to their peak amplitudes.
            signal = signal / norm(np.array(signal))
            template = template / norm(np.array(template))

            try:
                delta_t = signal.delta_t
            except BaseException:
                delta_t = 1
                signal = pycbc.types.timeseries.TimeSeries(signal, delta_t)

            try:
                delta_t = template.delta_t
            except BaseException:
                delta_t = 1
                template = pycbc.types.timeseries.TimeSeries(template, delta_t)

            # message(type(signal), type(template))
            # Align the waveforms in phase
            signal_al, template_al = pycbc.waveform.utils.coalign_waveforms(signal, template)

            # message(np.where(np.array(signalp_al)!=0))

            # Compute the match score
            (matchscore, finalshift) = pycbc.filter.matchedfilter.match(signal_al, template_al)

            # message('+ The match score, shift are %f, %d'%(matchscore, finalshift))
            matchdet.append({"Waveforms": [signal_al, template_al], "Match score": matchscore, "Shift": finalshift})

        return matchdet

    def roll(tsdata, i_roll, is_ts=False):
        """Roll the data circularly. Circular counterpart of shiftmatched function.

        Parameters
        ----------

        tsdata: 1d array or pycbc TimeSeries
                                1D data vector in the form of a list/ numpy array or timeseries.
        i_roll: int
                                The number of indices to roll the array.

        Returns
        -------

        rolled_waveform:    1d array or(pycbc TimeSeries object
                                                The rolled wavefrom.
        """

        flag = 0
        if is_ts:
            try:
                # Assign the time step.
                delta_t = tsdata.delta_t
                flag = 1
            except BaseException:
                flag = 0

        # Assign the data array
        tsdata = np.array(tsdata)
        # Break the array into two parts as last i + first i entries.
        arr1 = tsdata[-i_roll:]
        arr2 = tsdata[:-i_roll]
        # Join the two arrays and return them
        if flag == 1:
            rolled_waveform = pycbc.types.timeseries.TimeSeries(
                np.transpose(np.concatenate((np.transpose(arr1), np.transpose(arr2)))), delta_t
            )

        else:
            rolled_waveform = np.transpose(np.concatenate((np.transpose(arr1), np.transpose(arr2))))

        return rolled_waveform

    ##################################################################
    """ Data soothening functions """
    ##################################################################

    def smoothen(func_x, win, order, xdata=None, to_plot=False):
        """Use the Savitzky-Golay Filter to smoothen the data.
        Show the plots if plot=`yes`.

        Parameters
        ----------

        func_x: 1d array
                                The y axis.
        win:    int
                                        Window for smoothening. Must be odd.
        order:  int
                                        The order of the polynomial used for interpolation.
        x:  1d array, optional.
            The 1D list or numpy array, to plot
            the smoothened function. Only required
            if to_plot=True.
        to_plot:    bool
                                                        True or False. Whether or not to display the plot.

        Returns
        -------

        ydata:  1d array
                        The Savgol filtered list.

        """

        # Apply the filter
        ydata = scipy.signal.savgol_filter(func_x, win, order)
        # Show plots
        if to_plot:
            plt.plot(xdata, func_x, label="data")
            plt.plot(xdata, ydata, label="smoothened data")
            plt.title("Smoothened data using Savitzky-Golay Filter")
            plt.grid(which="both", axis="both")
            plt.legend()
            plt.show()
        # Returns the filtered data
        return ydata

    def bintp(xdata, func_x, width, order, to_plot=True):
        """Function to bin the data and interpolate it at specified width and order.

        Parameters
        ----------

        xdata:  1d array
                        1D list or numpy array.
        func_x: 1d array
                                        The y axis.
        width:  int
                                        Window size for smoothening,
        order:  int
                                        Order of the polynomial used for interpolation.
        to_plot:    bool
                                                        True or False. To plot or not plot the results.

        Returns
        -------

        hist:   a list
                [binloc, yvals], The location of the bins and
                the y values associated with the bins.

        """

        # Interpolation orders
        kind = [0, "linear", "quadratic", "cubic"]
        # Parse width
        width = int(width)
        # Number of bins
        nbins = int(len(xdata) / width)
        # Location of the bins
        binloc = [np.mean(xdata[width * index : width * index + width]) for index in range(0, nbins + 1)]
        # message(binloc)
        # Assigning y values to the bins
        yvals = [np.mean(func_x[width * index : width * index + width]) for index in range(0, nbins + 1)]
        # Assigning x values to the smoothened data
        # xf=x[width:-(width)/2]
        y_final = yvals
        # Interpolate if specified order is more than 0
        if order != 0:
            y_interp_func = scipy.interpolate.interp1d(binloc, yvals, kind=kind[order])
        # Reassign yf
        y_final = y_interp_func(binloc)
        # Set xf to binloc if order=0
        # if order==0:
        #       xf=binloc
        # y = signal.savgol_filter(func_x,win,order)
        if to_plot:
            # Plot the filtered data
            plt.plot(xdata, func_x, label="data")
            plt.plot(binloc, y_final, label="smoothened data")
            plt.title("Smoothened data by binning and interpolation")
            plt.grid(which="both", axis="both")
            plt.legend()
            plt.show()
        hist = [binloc, y_final]
        # Returns a list consisting of bin loacations and the correspnding y values
        return hist

    def mavg(func_x, width):
        """Function to smoothen data. Moving average over the window width.

        Parameters
        ----------

        func_x: 1d array
                                A list or numpy array of y axis.
        Width:  int
                        The width of the moving average window.

        Returns
        -------

        func_x_avgd:    1d array
                                        1D array of moving averaged y axis.

        """

        # message(len(func_x))
        # List to store smoothened data
        func_x_avgd = []
        # Calculate the moving-average upto last but width num of points
        for j in range(0, len(func_x) - width):
            func_x_avgd.append(np.mean(func_x[j : width + j]))
        # Calculate the moving-averaged values for the last width num of points
        for j in range(len(func_x) - width, len(func_x)):
            func_x_avgd.append(np.mean(func_x[j:]))
        # Returns the list containing the moving-averaged values
        return func_x_avgd

    # <Interpola

    def interpolate_wfs(ts_data, interp_func, delta_t=None, **kwargs):
        """Function to interpolate a list of timeseries data using
        the user specified interp_func function and the keyword arguments.

        Parameters
        ----------

        ts_data:    list
                    The 1d data. A list of waveforms
                    as a list or numpy array or
                    pycbc TimeSeries.
        interp_fun: function
                                                                        An interpolating function.
        delta_t:    float
                                        Timestep.
        ``**kwargs``:   keyword arguments
                                                                        additional arguments to the user specified interp_func.

        Returns
        -------

        interp_data:    list
                                                                        A list containing interpolated data.

        """

        # List for storing the interpolated data function
        interp_data = []
        # Loop over items in input
        for wfs in ts_data:
            if not delta_t:
                try:
                    # Find sampling time_step
                    delta_t = wfs.delta_t
                    timeaxis = wfs.sample_times

                except BaseException:
                    message("Input is not a TimeSeries. Please supply gridspacing as delta_t", message_verbosity=0)
            else:
                timeaxis = np.arange(0, len(wfs) * delta_t, delta_t)

            # Interpolate using the supplied function. The keyword arguments
            # supplied to the fuction are the keyword arguments to be supplied
            # to the interpolating function)
            # Append to the list of interpolated data function
            interp_data.append(interp_func(timeaxis, np.array(wfs), **kwargs))
        # Return the interpolated data function list
        return interp_data

    def resample(interp_data, new_delta_t, epoch, length, old_delta_t=None):
        """Function to generate timeseries out of the given interpolated data
        function, epoch,sampling frequency, length(duration).

        Parameters
        ----------

        interp_data:    1d array
                                                                        The yaxis to be interpolated.
        epoch:  float
                                        The starting point in time.
        delta_t:    float
                                        New grid spacing to be sampled at.
        length: int
                                                        The duration of x axis.

        Returns
        -------

        data:   list
                                        A list containing resampled data as pycbc TimeSeries.
        """

        data = []
        # Loop over objects in interp_data
        for i in range(len(interp_data)):
            if not old_delta_t:
                try:
                    old_delta_t = interp_data[i].delta_t

                except BaseException:
                    message("Input is not a TimeSeries. Please supply gridspacing as delta_t", message_verbosity=0)
            else:
                interp_data[i] = pycbc.types.timeseries.TimeSeries(interp_data[i], old_delta_t)

            # Prepare timeaxis
            timeaxis = np.linspace(epoch, epoch + length, int(length / new_delta_t))
            # Append the timeseries to the data list
            ydata = interp_data[i](timeaxis)
            data.append(pycbc.types.timeseries.TimeSeries(ydata, new_delta_t, epoch=epoch))
        # Return the list of samples timeseries
        return data

    def interp_resam_wfs(wavf_data, old_taxis, new_taxis, resam_kind="cubic"):
        """Wrapper function for interpolation and resampling.

        Parameters
        ----------

        wavf_data:  1d array
                    The yaxis to be interpolated,
        old_taxis, new_taxis:   1darray
                                Old and New time axis.

        Returns
        -------
        resam_wf_data:  1d array
                        Interpolated and resampled data.
        """
        from scipy.interpolate import interp1d

        # Interpolate
        interp_data = interp1d(old_taxis, wavf_data, kind=resam_kind)

        # Resample
        resam_wf_data = interp_data(new_taxis)

        return resam_wf_data

    def progressbar(present_count, total_counts, normalize="yes"):
        """Display the progress bar to std out from present_count and total_count.

        Parameters
        ----------

        present_count:  int
                                                                        The present count state.
        total_counts:   int
                                                                        The final state.

        Returns
        -------

        1 : int
                        The progress bar is messageed to stdout.

        """

        if normalize == "yes":
            final_progress = 98
            normalized_total_counts = final_progress * 10
            present_count = int(normalized_total_counts * present_count / total_counts)
            total_counts = normalized_total_counts

        # present_count = comm.gather(count,root=rank)
        else:
            final_progress = int(total_counts / 10)

        present_stage = int(present_count / 10)
        # message(present_stage)
        # message(total_counts)
        if present_stage != 0:
            if present_count == total_counts:
                present_semi_progress = "#"
            else:
                present_semi_progress = present_count % 10
        else:
            present_semi_progress = present_count
        offset = final_progress - present_stage
        per_cent_progress = 100 * present_count / total_counts

        sys.stdout.write(
            "\r"
            + "Progress:|"
            + "#" * present_stage
            + "%s" % present_semi_progress
            + " " * (offset)
            + "|"
            + "%.f%%" % per_cent_progress
        )
        sys.stdout.flush()
        return 1