Ejemplo n.º 1
0
    def calculate_radiation_pattern(self, az: float) -> None:
        """
        calculate the radiation pattern 3*3 array

        :param az: station azimuth in degree measured from the North (clockwise)
        :type az: float
        :raises PyfkError: length of source_mechanism must be 1, 3, 4, 7
        """
        mt = np.zeros((3, 3))
        if len(self._source_mechanism) == 1:
            self._m0 = self._source_mechanism[0] * 1e-20
            self._nn = 1
            self._rad = None
        elif len(self._source_mechanism) == 3:
            self._m0 = self._source_mechanism[0] * 1e-15
            self._nn = 2
            mt[0, :2] = self._source_mechanism[1:3]
            self._rad = sf_radiat(az - mt[0, 0], mt[0, 1])
        elif len(self._source_mechanism) == 4:
            self._m0 = np.power(10.,
                                1.5 * self._source_mechanism[0] + 16.1 - 20)
            self._nn = 3
            mt[0, :] = self._source_mechanism[1:]
            self._rad = dc_radiat(az - mt[0, 0], mt[0, 1], mt[0, 2])
        elif len(self._source_mechanism) == 7:
            self._m0 = self._source_mechanism[0] * 1e-20
            self._nn = 3
            mt[0, :] = self._source_mechanism[1:4]
            mt[1, 1:] = self._source_mechanism[4:6]
            mt[2, 2] = self._source_mechanism[6]
            self._rad = mt_radiat(az, mt)
        else:
            # actually will never satisfied in real case
            raise PyfkError("length of source_mechanism must be 1, 3, 4, 7")
Ejemplo n.º 2
0
    def __init__(
            self,
            sdep: float = 0.,
            srcType: str = "dc",
            source_mechanism: Optional[Union[list,
                                             np.ndarray]] = None) -> None:
        """
        the information about the the source used in the FK

        :param sdep: the depth of the source (km), and it should not be located between the interfaces of the Earth model, defaults to 0
        :type sdep: float
        :param srcType: the source type, can be ep (explosion), sf (single force) or dc (double couple), defaults to "dc"
        :type srcType: str, optional
        :param source_mechanism: a list with length of 1 (ep), 3 (sf), 4 or 7 (dc) with the same order as FK, or a Event of obspy (can read CMT solution file using obspy.read_events), defaults to None
        :type source_mechanism: Optional[Union[list, np.ndarray]], optional
        :raises PyfkError: [description]
        """
        self._sdep = sdep
        self._srcType = srcType

        if self._srcType not in ["dc", "sf", "ep"]:
            raise PyfkError(
                "Source type should be one of 'dc', 'sf', or 'ep'.")

        self._source_mechanism = None
        self._update_source_mechanism(source_mechanism)
Ejemplo n.º 3
0
    def _update_source_mechanism(
            self, source_mechanism: Optional[Union[list, np.ndarray]]):
        """
        the internal function to update the source mechanism information

        :param source_mechanism: a list with length of 1 (ep), 3 (sf), 4 or 7 (dc) with the same order as FK, or a Event of obspy (can read CMT solution file using obspy.read_events)
        :type source_mechanism: Optional[Union[list, np.ndarray]]
        :raises PyfkError: source_mechanism should be a 1D array
        :raises PyfkError: length of source_mechanism is not correct
        :raises PyfkError: source_mechanism must be None, a list or numpy.ndarray
        """
        self._source_mechanism: Optional[np.ndarray]
        if isinstance(source_mechanism, list) or isinstance(
                source_mechanism, np.ndarray):
            if len(np.shape(source_mechanism)) != 1:
                raise PyfkError("source_mechanism should be a 1D array")
            typemapper = {"dc": [4, 7], "sf": [3], "ep": [1]}
            if len(source_mechanism) not in typemapper[self._srcType]:
                raise PyfkError("length of source_mechanism is not correct")
            self._source_mechanism = np.array(source_mechanism)
        elif isinstance(source_mechanism, Event):
            tensor: Tensor = source_mechanism.focal_mechanisms[
                0].moment_tensor.tensor
            # convert the tensor in RTP(USE) to NED, refer to
            # https://gfzpublic.gfz-potsdam.de/rest/items/item_272892/component/file_541895/content
            # page4
            # * the reason why we mul 1e-19 here is to keep the value the same as global cmt website standard format
            m_zz = tensor.m_rr * 1e-19
            m_xx = tensor.m_tt * 1e-19
            m_yy = tensor.m_pp * 1e-19
            m_xz = tensor.m_rt * 1e-19
            m_yz = -tensor.m_rp * 1e-19
            m_xy = -tensor.m_tp * 1e-19
            m0 = source_mechanism.focal_mechanisms[
                0].moment_tensor.scalar_moment * 1e7
            self._source_mechanism = np.array(
                [m0, m_xx, m_xy, m_xz, m_yy, m_yz, m_zz])
        elif source_mechanism is None:
            self._source_mechanism = None
        else:
            raise PyfkError(
                "source_mechanism must be None, a list or numpy.ndarray")
Ejemplo n.º 4
0
    def update_source_mechanism(self, source_mechanism: Union[list, np.ndarray,
                                                              Event]):
        """
        update the source mechanism information after creation of the SourceModel

        :param source_mechanism: a list with length of 1 (ep), 3 (sf), 4 or 7 (dc) with the same order as FK, or a Event of obspy (can read CMT solution file using obspy.read_events)
        :type source_mechanism: Union[list, np.ndarray, Event]
        :raises PyfkError: source mechanism couldn't be None
        """
        if source_mechanism is None:
            raise PyfkError("source mechanism couldn't be None")
        self._update_source_mechanism(source_mechanism)
Ejemplo n.º 5
0
    def _couple_model_and_source(self) -> None:
        # * dealing with the coupling with the source and the seismodel
        if self.model.flattening:
            self.source.sdep = self._flattening_func(self.source.sdep)
            self.rdep = self._flattening_func(self.rdep)

        # * get src_layer and rcv_layer
        free_surface: bool = self.model.th[0] > 0
        if len(self.model.th) < 2:
            free_surface = True
        if free_surface and (self.source.sdep < 0 or self.rdep < 0):
            raise PyfkError("The source or receivers are located in the air.")
        if self.source.sdep < self.rdep:
            self.src_layer = self._insert_intf(self.source.sdep)
            self.rcv_layer = self._insert_intf(self.rdep)
        else:
            self.rcv_layer = self._insert_intf(self.rdep)
            self.src_layer = self._insert_intf(self.source.sdep)
        # two src layers should have same vp
        if (self.model.vp[self.src_layer] != self.model.vp[self.src_layer - 1]
            ) or (self.model.vs[self.src_layer] !=
                  self.model.vs[self.src_layer - 1]):
            raise PyfkError("The source is located at a real interface.")
Ejemplo n.º 6
0
def _waveform_integration(
        nfft2: int,
        dw: float,
        pmin: float,
        dk: float,
        kc: float,
        pmax: float,
        receiver_distance: np.ndarray,
        wc1: int,
        vs: np.ndarray,
        vp: np.ndarray,
        qs: np.ndarray,
        qp: np.ndarray,
        flip: bool,
        filter_const: float,
        dynamic: bool,
        wc2: int,
        t0: int,
        src_type: int,
        taper: float,
        wc: int,
        mu: np.ndarray,
        thickness: np.ndarray,
        si: np.ndarray,
        src_layer: int,
        rcv_layer: int,
        updn: int,
        epsilon: float,
        sigma: float,
        sum_waveform: np.ndarray,
        cuda_divide_num: int):
    # * generate kp and ks array ((nfft2-wc1+1)*len(thickness))
    kp_list = np.zeros((nfft2-wc1+1, len(thickness)), dtype=complex)
    ks_list = np.zeros((nfft2-wc1+1, len(thickness)), dtype=complex)

    # * get the n list, kp and ks
    n_list: np.ndarray = np.zeros(nfft2-wc1+1, dtype=int)
    n_list_accumulate: np.ndarray = np.zeros(nfft2-wc1+1, dtype=int)
    get_n_list_kpks(wc1, nfft2, kc, dw, pmin, pmax, dk, sigma, thickness, vp, vs, qp, qs,
                    n_list, n_list_accumulate, kp_list, ks_list)
    n_all: int = n_list_accumulate[-1]

    for index_cuda_divide in range(cuda_divide_num):
        # * current index_cuda_divide info
        current_range_list = np.array_split(range(n_all), cuda_divide_num)[
            index_cuda_divide]
        current_n_all = len(current_range_list)
        current_offset = current_range_list[0]

        # * generate the ik and i list representing the i wavenumber and i frequency
        ik_list = np.zeros(current_n_all, dtype=int)
        i_list = np.zeros(current_n_all, dtype=int)

        # * call fill_vals cuda kernel
        threadsperblock = 128
        blockspergrid = (current_n_all + (threadsperblock - 1)
                         ) // threadsperblock

        ik_list_d = cuda.to_device(ik_list)
        i_list_d = cuda.to_device(i_list)
        fill_vals[blockspergrid, threadsperblock](
            n_list, n_list_accumulate, ik_list_d, i_list_d, wc1, nfft2, current_n_all, current_offset)
        ik_list = ik_list_d.copy_to_host()
        i_list = i_list_d.copy_to_host()

        # * initialize the big matrix u (current_n_all*3*3)
        u: np.ndarray = np.zeros((current_n_all, 3, 3), dtype=complex)

        # * init cuda arrays
        # u, ik_list, i_list, kp, ks, thickness, mu, si
        try:
            u_d = cuda.to_device(u)
            ik_list_d = cuda.to_device(ik_list)
            i_list_d = cuda.to_device(i_list)
            kp_list_d = cuda.to_device(kp_list)
            ks_list_d = cuda.to_device(ks_list)
            thickness_d = cuda.to_device(thickness)
            mu_d = cuda.to_device(mu)
            si_d = cuda.to_device(si)
        except cuda.cudadrv.driver.CudaAPIError:
            from sys import getsizeof
            total_size = getsizeof(u)+getsizeof(ik_list)+getsizeof(i_list)+getsizeof(
                kp_list)+getsizeof(ks_list)+getsizeof(thickness)+getsizeof(mu)+getsizeof(si)
            raise PyfkError(
                f"You are creating {total_size} MB data, try to make CUDA_DIVIDE_NUM larger!")

        # * run the cuda kernel function
        parallel_kernel[blockspergrid, threadsperblock](u_d, ik_list_d, i_list_d, kp_list_d, ks_list_d, thickness_d, mu_d, si_d,
                                                        dw, pmin, dk, src_layer, rcv_layer, updn, src_type, epsilon, wc1, current_n_all)
        u = u_d.copy_to_host()

        # * get sum_waveform
        flip_val = 0
        if flip:
            flip_val = -1.
        else:
            flip_val = 1.

        z_list = np.zeros(
            (current_n_all, len(receiver_distance)), dtype=float)
        get_z_list(z_list, ik_list, i_list, receiver_distance,
                   dw, pmin, dk, current_n_all)
        aj0_list = cal_cujn(0, z_list)
        aj1_list = cal_cujn(1, z_list)
        aj2_list = cal_cujn(2, z_list)
        # it's not appropriate to use cuda here, as it will use large atomic operation.
        get_sum_waveform(sum_waveform, u, ik_list, receiver_distance,  flip_val,
                         z_list, aj0_list, aj1_list, aj2_list, current_n_all)

    # * perform the filtering
    apply_filter(wc1, nfft2, dw, filter_const, dynamic, wc, taper, wc2,
                 receiver_distance, t0, sum_waveform)
Ejemplo n.º 7
0
    def __init__(self,
                 model: Optional[SeisModel] = None,
                 source: Optional[SourceModel] = None,
                 receiver_distance: Optional[Union[list, np.ndarray]] = None,
                 planet_radius: float = 6371.,
                 degrees: bool = False,
                 taper: float = 0.3,
                 filter: Tuple[float, float] = (0, 0),
                 npt: int = 256,
                 dt: float = 1.,
                 dk: float = 0.3,
                 smth: float = 1.,
                 pmin: float = 0.,
                 pmax: float = 1.,
                 kmax: float = 15.,
                 rdep: float = 0.,
                 updn: str = "all",
                 samples_before_first_arrival: int = 50,
                 suppression_sigma: float = 2.,
                 cuda: bool = False) -> None:
        """
        The configuration class used in generating Green's function and the synthetic waveform.

        :param model: the Earth model used in calculation, defaults to None
        :type model: Optional[SeisModel]
        :param source: the source model used in calculation, defaults to None
        :type source: Optional[SourceModel]
        :param receiver_distance: a list of receiver distance in km, defaults to None
        :type receiver_distance: Optional[Union[list, np.ndarray]]
        :param degrees: use degrees instead of km, defaults to False
        :type degrees: bool, optional
        :param planet_radius: the radius of the planet in km, default to 6371.
        :type planet_radius: float, optional
        :param taper: taper applies a low-pass cosine filter at fc=(1-taper)*f_Niquest, defaults to 0.3
        :type taper: float, optional
        :param filter: apply a high-pass filter with a cosine transition zone between freq. f1 and f2 in Hz, defaults to (0, 0)
        :type filter: Tuple[float, float], optional
        :param npt: the number of points, defaults to 256
        :type npt: int, optional
        :param dt: the sampling interval in seconds, defaults to 1.
        :type dt: float, optional
        :param dk: the non-dimensional sampling interval of wavenumber, defaults to 0.3
        :type dk: float, optional
        :param smth: makes the final sampling interval to be dt/smth, defaults to 1.
        :type smth: float, optional
        :param pmin: the min slownesses in term of 1/vs_at_the_source, defaults to 0.
        :type pmin: float, optional
        :param pmax: the max slownesses in term of 1/vs_at_the_source, defaults to 1.
        :type pmax: float, optional
        :param kmax: kmax at zero frequency in term of 1/hs, defaults to 15.
        :type kmax: float, optional
        :param rdep: the depth for the receivers in km, defaults to 0.
        :type rdep: float, optional
        :param updn: "up" for up-going wave only, "down" for down-going wave only, "all" for both "up" and "down", defaults to "all"
        :type updn: str, optional
        :param samples_before_first_arrival: the number of points before the first arrival, defaults to 50
        :type samples_before_first_arrival: int, optional
        :param suppression_sigma: the suppression factor of the numerical noise, defaults to 2
        :type suppression_sigma: float, optional
        :param cuda: whether to use the cuda mode. if set PYFK_USE_CUDA=1, this flag will be ignored and will always use cuda, defaults to False
        :type cuda: bool
        :raises PyfkError: Must provide a list of receiver distance
        :raises PyfkError: Can't set receiver distance as 0, please consider to use a small value instead
        :raises PyfkError: planet_radius should be positive
        :raises PyfkError: Taper must be with (0,1)
        :raises PyfkError: Filter must be a tuple (f1,f2), f1 and f2 should be within [0,1]
        :raises PyfkError: npt should be positive.
        :raises PyfkError: dt should be positive.
        :raises PyfkError: dk should be within (0,0.5)
        :raises PyfkError: smth should be positive.
        :raises PyfkError: pmin should be within [0,1]
        :raises PyfkError: pmax should be within [0,1]
        :raises PyfkError: pmin should be smaller than pmax
        :raises PyfkError: kmax should be larger or equal to 10
        :raises PyfkError: the selection of phases should be either 'up', 'down' or 'all'
        :raises PyfkError: samples_before_first_arrival should be positive
        :raises PyfkError: suppression_sigma should be positive
        :raises PyfkError: Must provide a source
        :raises PyfkError: Must provide a seisModel
        """
        # * read in and validate parameters
        # receiver_distance
        if receiver_distance is None:
            raise PyfkError("Must provide a list of receiver distance")
        self.receiver_distance: np.ndarray = np.array(receiver_distance,
                                                      dtype=np.float64)
        if 0 in self.receiver_distance:
            raise PyfkError(
                "Can't set receiver distance as 0, please consider to use a small value instead"
            )
        # planet_radius
        if planet_radius <= 0:
            raise PyfkError("planet_radius should be positive")
        self.planet_radius = planet_radius
        # degrees
        if degrees:
            self.receiver_distance = np.array(
                list(
                    map(
                        lambda dis: degrees2kilometers(
                            dis, radius=self.planet_radius),
                        self.receiver_distance)))
        # taper
        if taper <= 0 or taper > 1:
            raise PyfkError("Taper must be with (0,1)")
        self.taper = taper
        # filter
        self.filter = filter
        # npt
        if npt <= 0:
            raise PyfkError("npt should be positive.")
        self.npt = npt
        if self.npt == 1:
            # we don't use st_fk
            self.npt = 2
        # dt
        if dt <= 0:
            raise PyfkError("dt should be positive.")
        if self.npt == 2 and dt < 1000:
            self.dt = 1000
        else:
            self.dt = dt
        # dk
        if dk <= 0 or dk >= 0.5:
            raise PyfkError("dk should be within (0,0.5)")
        if dk <= 0.1 or dk >= 0.4:
            warnings.warn(
                PyfkWarning("dk is recommended to be within (0.1,0.4)"))
        self.dk = dk
        # smth
        if smth <= 0:
            raise PyfkError("smth should be positive.")
        self.smth = smth
        # pmin
        if pmin < 0 or pmin > 1:
            raise PyfkError("pmin should be within [0,1]")
        self.pmin = pmin
        # pmax
        if pmax < 0 or pmax > 1:
            raise PyfkError("pmax should be within [0,1]")
        if pmin >= pmax:
            raise PyfkError("pmin should be smaller than pmax")
        self.pmax = pmax
        # kmax
        if kmax < 10:
            raise PyfkError("kmax should be larger or equal to 10")
        self.kmax = kmax
        # rdep
        self.rdep = rdep
        # updn
        if updn not in ["all", "up", "down"]:
            raise PyfkError(
                "the selection of phases should be either 'up', 'down' or 'all'"
            )
        self.updn = updn
        # samples_before_first_arrival
        if samples_before_first_arrival <= 0:
            raise PyfkError("samples_before_first_arrival should be positive")
        self.samples_before_first_arrival = samples_before_first_arrival
        # suppression_sigma
        if suppression_sigma <= 0:
            raise PyfkError("suppression_sigma should be positive")
        self.suppression_sigma = suppression_sigma
        # cuda
        self.cuda = cuda
        # source and model
        if (source is None) or (not isinstance(source, SourceModel)):
            raise PyfkError("Must provide a source")
        if (model is None) or (not isinstance(model, SeisModel)):
            raise PyfkError("Must provide a seisModel")
        self.source = source
        # use copy since the model will be modified
        self.model = copy(model)
        self._couple_model_and_source()
Ejemplo n.º 8
0
    def __init__(self,
                 model: np.ndarray = None,
                 flattening: bool = False,
                 use_kappa: bool = False,
                 r_planet: Optional[float] = None) -> None:
        """
        SeisModel stores the Earth model

        :param model: numpy.ndarray model: a 2D numpy array storing the information of the 1D earth model, with the same format as FK.
               model has the following format (in units of km, km/s, g/cm3, each column):
               thickness vs vp_or_vp/vs [rho Qs Qp]
               rho=0.77 + 0.32*vp if not provided or the 4th column is larger than 20 (treated as Qs).
               Qs=500, Qp=2*Qs, if they are not specified.
               If the first layer thickness is zero, it represents the top elastic half-space.
               Otherwise, the top half-space is assumed to be vacuum and does not need to be specified.
               The last layer (i.e. the bottom half space) thickness should be always be zero. (if not, will use 0 anyway), defaults to None
        :type model: np.ndarray
        :param flattening: if the model and the source should be flatten, defaults to False
        :type flattening: bool, optional
        :param use_kappa: if the third column of the model file is vp/vs ratio, defaults to False
        :type use_kappa: bool, optional
        :param r_planet: the optional defined planet radius in KM. If not defined, the value will be None and the Earth radius = 6371.0 km will be used
        :type r_planet: float, optional
        :raises PyfkError: Earth Model must be a 2D numpy array
        :raises PyfkError: Must provide at least three columns for the model
        :raises PyfkError: r_planet should be a float or integer number
        :raises PyfkError: r_planet must be a positive value
        """
        if not isinstance(model, np.ndarray):
            raise PyfkError("Earth Model must be a 2D numpy array.")
        if len(np.shape(model)) != 2:
            raise PyfkError("Earth Model must be a 2D numpy array.")

        self._flattening = flattening

        row: int
        column: int
        row, column = np.shape(model)
        if column < 3:
            raise PyfkError(
                "Must provide at least three columns for the model")
        self.model_values: np.ndarray = np.zeros((row, 6), dtype=float)
        # * ensure the planet radius before flattening
        # r_planet
        self.r_planet = None
        if r_planet != None:
            if not (isinstance(r_planet, float) or isinstance(r_planet, int)):
                raise PyfkError("r_planet should be a float or integer number")
            if r_planet <= 0:
                raise PyfkError("r_planet must be a positive value")
            self.r_planet = r_planet
        if self.r_planet == None:
            self.r_planet = R_EARTH
        # * read model values and apply flattening
        fl: np.ndarray = np.ones(row, dtype=float)
        if self._flattening:
            r = self.r_planet
            for irow in range(row):
                r = r - model[irow, 0]
                fl[irow] = self.r_planet / (r + 0.5 * model[irow, 0])
        self.model_values[:, 0] = model[:, 0] * fl
        # set the thickness for the last row as 0
        self.model_values[-1, 0] = 0.
        self.model_values[:, 1] = model[:, 1] * fl
        if use_kappa:
            # already have fl in self.model_values[:, 1]
            self.model_values[:, 2] = model[:, 2] * self.model_values[:, 1]
        else:
            self.model_values[:, 2] = model[:, 2] * fl
        # * handle other columns
        if column == 3:
            self.model_values[:, 3] = 0.77 + 0.32 * self.model_values[:, 2]
            self.model_values[:, 4] = 500.
            self.model_values[:, 5] = 2 * self.model_values[:, 4]
        elif column == 4:
            if np.any(model[:, 3] > 20.):
                self.model_values[:, 3] = 0.77 + 0.32 * self.model_values[:, 2]
                self.model_values[:, 4] = model[:, 3]
                self.model_values[:, 5] = 2 * self.model_values[:, 4]
            else:
                self.model_values[:, 3] = model[:, 3]
                self.model_values[:, 4] = 500.
                self.model_values[:, 5] = 2 * self.model_values[:, 4]
        elif column == 5:
            self.model_values[:, 3:5] = model[:, 3:5]
            self.model_values[:, 5] = 2 * self.model_values[:, 4]
        elif column == 6:
            self.model_values[:, 3:] = model[:, 3:]
Ejemplo n.º 9
0
def calculate_sync(
        gf: Union[List[Stream], Stream],
        config: Config,
        az: Union[float, int] = 0,
        source_time_function: Optional[Trace] = None) -> List[Stream]:
    """
    Compute displacements in cm in the up, radial (outward), and transverse (clockwise) directions produced by different seismic sources

    :param gf: the Green's function list (or just an obspy Stream representing one distance) from calculate_gf; or read from the Green's function database from FK
    :type gf: Union[List[Stream], Stream]
    :param config: the configuration of sync calculation
    :type config: Config
    :param az: set station azimuth in degree measured from the North, defaults to 0
    :type az: Union[float, int], optional
    :param source_time_function: should be an obspy Trace with the data as the source time function, can use generate_source_time_function to generate a trapezoid shaped source time function, defaults to None
    :type source_time_function: Optional[Trace], optional
    :raises PyfkError: az must be a number
    :raises PyfkError: must provide a source time function
    :raises PyfkError: check input Green's function
    :raises PyfkError: delta for the source time function and the Green's function should be the same
    :return: a list of three components stream (ordered as z, r, t)
    :rtype: List[Stream]
    """
    # * handle input parameters
    if not (isinstance(az, float) or isinstance(az, int)):
        raise PyfkError("az must be a number")
    az = az % 360
    cmpaz_list = np.array([0., az, az + 90])
    if cmpaz_list[2] > 360.:
        cmpaz_list[2] -= 360.
    if isinstance(gf, Stream):
        gf = [gf]
    if source_time_function is None:
        raise PyfkError("must provide a source time function")
    if (not isinstance(gf, list)) or (len(gf)
                                      == 0) or (not isinstance(gf[0], Stream)):
        raise PyfkError("check input Green's function")
    for irec in range(len(gf)):
        for each_trace in gf[irec]:
            if each_trace.stats["delta"] != source_time_function.stats["delta"]:
                raise PyfkError(
                    "delta for the source time function and the Green's function should be the same"
                )

    # * calculate the radiation pattern
    config.source.calculate_radiation_pattern(az)

    # * handle gf, project to the three component with amplitude from radiation pattern
    sync_gf = sync_calculate_gf(gf, config.source)

    # * the main loop to conv the source with the gf
    sync_result = []
    cmpinc_list = [0., 90., 90.]
    for irec in range(len(sync_gf)):
        sync_result.append(Stream())
        for icom in range(3):
            # do the convolution
            data_conv = np.convolve(
                source_time_function.data,
                sync_gf[irec][icom].data)[:len(sync_gf[irec][icom].data)]
            header = {**sync_gf[irec][icom].stats}
            if "sac" not in header:
                # if called after gf, should never happen
                header["sac"] = {}
            header["sac"]["az"] = az
            header["sac"]["cmpinc"] = cmpinc_list[icom]
            header["sac"]["cmpaz"] = cmpaz_list[icom]
            sync_result[-1] += Trace(header=header, data=data_conv)
    return sync_result