コード例 #1
0
def zero_pad(st, pad_length_in_seconds, before=True, after=True):
    """
    Zero pad the data of a stream, change the starttime to reflect the change.
    Useful for if e.g. observed data starttime comes in later than synthetic.

    :type st: obspy.stream.Stream
    :param st: stream to be zero padded
    :type pad_length_in_seconds: int
    :param pad_length_in_seconds: length of padding front and back
    :type before: bool
    :param before: pad the stream before the origin time
    :type after: bool
    :param after: pad the stream after the last sample
    :rtype st: obspy.stream.Stream
    :return st: stream with zero padded data object
    """
    pad_before, pad_after = 0, 0
    st_pad = st.copy()
    for tr in st_pad:
        array = tr.data
        pad_width = int(pad_length_in_seconds * tr.stats.sampling_rate)
        # Determine if we should pad before or after
        if before:
            pad_before = pad_width
        if after:
            pad_after = pad_width
        logger.debug(f"zero pad {tr.id} ({pad_before}, {pad_after}) samples")
        # Constant value is default 0
        tr.data = np.pad(array, (pad_before, pad_after), mode='constant')
        tr.stats.starttime -= pad_length_in_seconds
        logger.debug(f"new starttime {tr.id}: {tr.stats.starttime}")

    return st_pad
コード例 #2
0
ファイル: load.py プロジェクト: bch0w/pyatoa
def load_windows(ds, net, sta, iteration, step_count, return_previous=False):
    """
    Returns misfit windows from an ASDFDataSet for a given iteration, step,
    network and station, as well as a count of windows returned.

    If given iteration and step are not present in dataset (e.g. during line 
    search, new step), will try to search the previous step, which may or 
    may not be contained in the previous iteration. 

    Returns windows as Pyflex Window objects which can be used in Pyadjoint or
    in the Pyatoa workflow.

    .. note::
        Expects that windows are saved into the dataset at each iteration and 
        step such that there is a coherent structure within the dataset

    :type ds: pyasdf.ASDFDataSet
    :param ds: ASDF dataset containing MisfitWindows subgroup
    :type net: str
    :param net: network code used to find the name of the misfit window
    :type sta: str
    :param sta: station code used to find the name of the misfit window
    :type iteration: int or str
    :param iteration: current iteration, will be formatted by the function
    :type step_count: int or str
    :param step_count: step count, will be formatted by the function
    :type return_previous: bool
    :param return_previous: search the dataset for available windows
        from the previous iteration/step given the current iteration/step
    :rtype window_dict: dict
    :return window_dict: dictionary containing misfit windows, in a format
        expected by Pyatoa Manager class
    """
    # Ensure the tags are properly formatted
    iteration = format_iter(iteration)
    step_count = format_step(step_count)
    windows = ds.auxiliary_data.MisfitWindows

    window_dict = {}
    if return_previous:
        # Retrieve windows from previous iter/step
        prev_windows = previous_windows(windows=windows,
                                        iteration=iteration,
                                        step_count=step_count)
        window_dict = dataset_windows_to_pyflex_windows(windows=prev_windows,
                                                        network=net,
                                                        station=sta)
    else:
        if hasattr(windows, iteration) and \
                            hasattr(windows[iteration], step_count):
            # Attempt to retrieve windows from the given iter/step
            logger.debug(f"searching for windows in {iteration}{step_count}")
            window_dict = dataset_windows_to_pyflex_windows(
                windows=windows[iteration][step_count],
                network=net,
                station=sta)

    return window_dict
コード例 #3
0
def stf_convolve(st,
                 half_duration,
                 source_decay=4.,
                 time_shift=None,
                 time_offset=None):
    """
    Convolve function with a Gaussian window source time function.
    Design follows Specfem3D Cartesian "comp_source_time_function.f90"

    `hdur` given is `hdur`_Gaussian = hdur/SOURCE_DECAY_MIMIC_TRIANGLE
    with SOURCE_DECAY_MIMIC_TRIANGLE ~ 1.68

    This gaussian uses a strong decay rate to avoid non-zero onset times, while
    still miicking a triangle source time function

    :type st: obspy.stream.Stream
    :param st: stream object to convolve with source time function
    :type half_duration: float
    :param half_duration: the half duration of the source time function,
        usually provided in moment tensor catalogs
    :type source_decay: float
    :param source_decay: the decay strength of the source time function, the
        default value of 4 gives a Gaussian. A value of 1.68 mimics a triangle.
    :type time_shift: float
    :param time_shift: Time shift of the source time function in seconds
    :type time_offset: If simulations have a value t0 that is negative, i.e. a
        starttime before the event origin time. This value will make sure the
        source time function doesn't start convolving before origin time to
        avoid non-zero onset times
    :rtype: obspy.stream.Stream
    :return: stream object which has been convolved with a source time function
    """
    logger.debug(f"convolving data w/ Gaussian (t/2={half_duration:.2f}s)")

    sampling_rate = st[0].stats.sampling_rate
    half_duration_in_samples = round(half_duration * sampling_rate)

    # generate gaussian function
    decay_rate = half_duration_in_samples / source_decay
    a = 1 / (decay_rate**2)
    t = np.arange(-half_duration_in_samples, half_duration_in_samples, 1)
    gaussian_stf = np.exp(-a * t**2) / (np.sqrt(np.pi) * decay_rate)

    # prepare time offset machinery
    if time_offset:
        time_offset_in_samp = int(time_offset * sampling_rate)

    # convolve each trace with the soure time function and time shift if needed
    st_out = st.copy()
    for tr in st_out:
        if time_shift:
            tr.stats.starttime += time_shift
        data_out = np.convolve(tr.data, gaussian_stf, mode="same")
        tr.data = data_out

    return st_out
コード例 #4
0
ファイル: load.py プロジェクト: bch0w/pyatoa
def previous_windows(windows, iteration, step_count):
    """
    Given an iteration and step count, find windows from the previous step
    count. If none are found for the given iteration, return the most recently
    available windows.

    .. note:: 
        Assumes that windows are saved at each iteration.

    :type windows: pyasdf.utils.AuxiliaryDataAccessor
    :param windows: ds.auxiliary_data.MisfitWindows[iter][step]
    :type iteration: int or str
    :param iteration: the current iteration
    :type step_count: int or str
    :param step_count: the current step count
    :rtype: pyasdf.utils.AuxiliaryDataAccessor
    :return: ds.auxiliary_data.MisfitWindows
    """
    # Ensure we're working with integer values for indexing, e.g. 's00' -> 0
    if isinstance(iteration, str):
        iteration = int(iteration[1:])
    if isinstance(step_count, str):
        step_count = int(step_count[1:])

    # Get a flattened list of iters and steps as unique tuples of integers
    iters = []
    steps = {i: windows[i].list() for i in windows.list()}
    for i, s in steps.items():
        for s_ in s:
            iters.append((int(i[1:]), int(s_[1:])))

    current = (iteration, step_count)
    if current in iters:
        # If windows have already been added to the auxiliary data
        prev_iter, prev_step = iters[iters.index(current) - 1]
    else:
        # Wind back the step to see if there are any windows for this iteration
        while step_count >= 0:
            if (iteration, step_count) in iters:
                prev_iter, prev_step = iteration, step_count
                break
            step_count -= 1
        else:
            # If nothing is found return the most recent windows available
            prev_iter, prev_step = iters[-1]

    # Format back into strings for accessing auxiliary data
    prev_iter = format_iter(prev_iter)
    prev_step = format_step(prev_step)

    logger.debug(f"most recent windows: {prev_iter}{prev_step}")

    return windows[prev_iter][prev_step]
コード例 #5
0
ファイル: load.py プロジェクト: bch0w/pyatoa
def dataset_windows_to_pyflex_windows(windows, network, station):
    """
    Convert the parameter dictionary of an ASDFDataSet MisfitWindow into a 
    dictionary of Pyflex Window objects, in the same format as Manager.windows

    Returns empty dict and 0 if no windows are found

    :type windows: pyasdf.utils.AuxiliaryDataAccessor
    :param windows: ds.auxiliary_data.MisfitWindows[iter][step]
    :type network: str
    :param network: network of the station related to the windows
    :type station: str
    :param station: station related to the windows
    :rtype: dict
    :return: dictionary of window attributes in the same format that Pyflex 
        outputs
    """
    window_dict, _num_windows = {}, 0
    for window_name in windows.list():
        net, sta, comp, n = window_name.split("_")

        # Check the title of the misfit window to see if applicable
        if (net == network) and (sta == station):
            par = windows[window_name].parameters

            # Create a Pyflex Window object
            window = Window(left=par["left_index"],
                            right=par["right_index"],
                            center=par["center_index"],
                            dt=par["dt"],
                            time_of_first_sample=UTCDateTime(
                                par["time_of_first_sample"]),
                            min_period=par["min_period"],
                            channel_id=par["channel_id"])

            # We cant initiate these parameters so set them after the fact
            # If data changed, should recalculate with Window._calc_criteria()
            setattr(window, "dlnA", par["dlnA"])
            setattr(window, "cc_shift", par["cc_shift_in_samples"])
            setattr(window, "max_cc_value", par["max_cc_value"])

            # Save windows into the dictionary labelled by component
            if comp in window_dict.keys():
                # Either append to existing entry
                window_dict[comp] += [window]
            else:
                # Or create the first entry
                window_dict[comp] = [window]
            _num_windows += 1

    logger.debug(f"{_num_windows} window(s) found in dataset for "
                 f"{network}.{station}")
    return window_dict
コード例 #6
0
def write_stations_adjoint(ds,
                           iteration,
                           specfem_station_file,
                           step_count=None,
                           pathout=None):
    """
    Generate the STATIONS_ADJOINT file for Specfem input by reading in the
    STATIONS file and cross-checking which adjoint sources are available in the
    Pyasdf dataset.
    
    :type ds: pyasdf.ASDFDataSet
    :param ds: dataset containing AdjointSources auxiliary data
    :type iteration: str or int
    :param iteration: iteration number, e.g. "i01". Will be formatted so int ok.
    :type step_count: str or int
    :param step_count: step count e.g. "s00". Will be formatted so int ok.
        If NoneType, final step of the iteration will be chosen automatically.
    :type specfem_station_file: str
    :param specfem_station_file: path/to/specfem/DATA/STATIONS
    :type pathout: str
    :param pathout: path to save file 'STATIONS_ADJOINT'
    """
    # Check which stations have adjoint sources
    stas_with_adjsrcs = []
    adj_srcs = ds.auxiliary_data.AdjointSources[format_iter(iteration)]
    # Dynamically determine final step count in the iteration
    if step_count is None:
        step_count = adj_srcs.list()[-1]
    logger.debug(f"writing stations adjoint for "
                 f"{format_iter(iteration)}{format_step(step_count)}")
    adj_srcs = adj_srcs[format_step(step_count)]

    for code in adj_srcs.list():
        stas_with_adjsrcs.append(code.split('_')[1])
    stas_with_adjsrcs = set(stas_with_adjsrcs)

    # Figure out which stations were simulated
    with open(specfem_station_file, "r") as f:
        lines = f.readlines()

    # If no output path is specified, save into current working directory with
    # an event_id tag to avoid confusion with other files, else normal naming
    if pathout is None:
        write_out = f"./STATIONS_ADJOINT_{format_event_name(ds)}"
    else:
        write_out = os.path.join(pathout, "STATIONS_ADJOINT")

    # Rewrite the Station file but only with stations that contain adjoint srcs
    with open(write_out, "w") as f:
        for line in lines:
            if line.split()[0] in stas_with_adjsrcs:
                f.write(line)
コード例 #7
0
def match_npts(st_a, st_b, force=None):
    """
    Resampling can cause sample number differences which will lead to failure
    of some preprocessing or processing steps. This function ensures that `npts` 
    matches between traces by extending one of the traces with zeros. 
    A small taper is applied to ensure the new values do not cause 
    discontinuities.

    Note:
        its assumed that all traces within a single stream have the same `npts`

    :type st_a: obspy.stream.Stream
    :param st_a: one stream to match samples with
    :type st_b: obspy.stream.Stream
    :param st_b: one stream to match samples with
    :type force: str
    :param force: choose which stream to use as the default npts,
        defaults to 'a', options: 'a', 'b'
    :rtype: tuple (obspy.stream.Stream, obspy.stream.Stream)
    :return: streams that may or may not have adjusted npts, returned in the 
        same order as provided
    """
    # Assign the number of points, copy to avoid editing in place
    if not force or force == "a":
        npts = st_a[0].stats.npts
        st_const = st_a.copy()
        st_change = st_b.copy()
    else:
        npts = st_b[0].stats.npts
        st_const = st_b.copy()
        st_change = st_a.copy()

    for tr in st_change:
        diff = abs(tr.stats.npts - npts)
        if diff:
            logger.debug(f"appending {diff} zeros to {tr.get_id()}")
            tr.data = np.append(tr.data, np.zeros(diff))

    # Ensure streams are returned in the correct order
    if not force or force == "a":
        return st_const, st_change
    else:
        return st_change, st_const
コード例 #8
0
    def save_windows(self):
        """
        Convenience function to save collected misfit windows into an 
        ASDFDataSet with some preliminary checks

        Auxiliary data tag is hardcoded as 'MisfitWindows'
        """
        if self.ds is None:
            logger.warning("Manager has no ASDFDataSet, cannot save windows")
        elif not self.windows:
            logger.warning("Manager has no windows to save")
        elif not self.config.save_to_ds:
            logger.warning("config parameter save_to_ds is set False, "
                           "will not save windows")
        else:
            logger.debug("saving misfit windows to ASDFDataSet")
            add_misfit_windows(self.windows,
                               self.ds,
                               path=self.config.aux_path)
コード例 #9
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def gather_event(self, event_id=None, try_fm=True, **kwargs):
        """
        Gather an ObsPy Event object by searching disk then querying webservices

        .. note::
            Event info need only be retrieved once per Pyatoa workflow.

        :type try_fm: bool
        :param try_fm: try to find correspondig focal mechanism.
        :rtype: obspy.core.event.Event 
        :return: event retrieved either via internal or external methods
        :raises GathererNoDataException: if no event information is found.
        """
        logger.debug("gathering event")
        event = None

        # Attempt to gather event information internally
        event = self.event_fetch(event_id, **kwargs)
        # If no data internally, query FDSN
        if event is None and self.Client:
            event = self.event_get(event_id)
            # Append focal mechanism or moment tensor information, which is
            # likely stored in a separate catalog
            if try_fm:
                event = append_focal_mechanism(event,
                                               client=self.config.client)
        # If no event after internal/external checks, throw error
        if event is None:
            raise GathererNoDataException(f"no Event information found for "
                                          f"{self.config.event_id}")
        # Otherwise state success and grab important origin information
        else:
            self.origintime = event.preferred_origin().time
        # Save event information to dataset if necessary
        if self.ds and self.config.save_to_ds:
            try:
                self.ds.add_quakeml(event)
                logger.debug(f"event QuakeML added to ASDFDataSet")
            # Trying to re-add an event to the ASDFDataSet throws ValueError
            except ValueError:
                pass
        return event
コード例 #10
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def asdf_event_fetch(self):
        """
        Return Event information from ASDFDataSet.

        .. note::
            Assumes that the ASDF Dataset will only contain one event, which is
            dictated by the structure of Pyatoa.
        .. note::
            TO DO:
            * Remove the logger statement, and write to corresponding functions
            * event_fetch: calls this function and (1) in succession

        :rtype event: obspy.core.event.Event
        :return event: event object
        :raises AttributeError: if no event attribute found in ASDFDataSet
        :raises IndexError: if event attribute found but no events
        """
        event = self.ds.events[0]
        logger.debug(f"matching event found: {format_event_name(event)}")
        return event
コード例 #11
0
    def save_adjsrcs(self):
        """
        Convenience function to save collected adjoint sources into an 
        ASDFDataSet with some preliminary checks

        Auxiliary data tag is hardcoded as 'AdjointSources'        
        """
        if self.ds is None:
            logger.warning("Manager has no ASDFDataSet, cannot save "
                           "adjoint sources")
        elif not self.adjsrcs:
            logger.warning("Manager has no adjoint sources to save")
        elif not self.config.save_to_ds:
            logger.warning("config parameter save_to_ds is set False, "
                           "will not save adjoint sources")
        else:
            logger.debug("saving adjoint sources to ASDFDataSet")
            add_adjoint_sources(adjsrcs=self.adjsrcs,
                                ds=self.ds,
                                path=self.config.aux_path,
                                time_offset=self.stats.time_offset_sec)
コード例 #12
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def event_get(self, event_id=None):
        """
        Return event information parameters pertaining to a given event id
        if an event id is given, else by origin time. Catches FDSN exceptions.

        :rtype event: obspy.core.event.Event or None
        :return event: event object if found, else None.
        """
        if not self.Client:
            return None
        if event_id is None:
            event_id = self.config.event_id

        event, origintime = None, None
        if event_id is not None:
            try:
                # Get events via event id, only available from certain clients
                logger.debug(f"event ID: {event_id}, querying "
                             f"client {self.config.client}")
                event = self.Client.get_events(eventid=event_id)[0]
            except FDSNException:
                pass
        if self.origintime and event is None:
            try:
                # If getting by event id doesn't work, try based on origintime
                logger.debug(f"origintime: {self.origintime}, querying"
                             f"client {self.config.client}")
                event = self.Client.get_events(starttime=self.origintime,
                                               endtime=self.origintime)
                if len(event) > 1:
                    # Getting by origin time may result in multiple events
                    # found in the catalog, this is hard to control and will
                    # probably need to be addressed manually.
                    logger.warning(f"{len(event)} events found, expected 1."
                                   f"Returning first entry, manual revision "
                                   f"may be required.")
                event = event[0]
            except FDSNException:
                pass
        return event
コード例 #13
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def station_get(self, code, **kwargs):
        """
        Call for ObsPy FDSN client to download station dataless information.
        Defaults to retrieving response information.

        :type code: str
        :param code: Station code following SEED naming convention.
            This must be in the form NN.SSSS.LL.CCC (N=network, S=station,
            L=location, C=channel). Allows for wildcard naming. By default
            the pyatoa workflow wants three orthogonal components in the N/E/Z
            coordinate system. Example station code: NZ.OPRZ.10.HH?
        :rtype: obspy.core.inventory.Inventory
        :return: inventory containing relevant network and stations

        Keyword Arguments
        ::
            str station_level:
                The level of the station metadata if retrieved using the ObsPy
                Client. Defaults to 'response'
        """
        level = kwargs.get("station_level", "response")

        if not self.Client:
            return None

        logger.debug(f"querying client {self.config.client}")
        net, sta, loc, cha = code.split('.')
        try:
            inv = self.Client.get_stations(
                network=net,
                station=sta,
                location=loc,
                channel=cha,
                starttime=self.origintime - self.config.start_pad,
                endtime=self.origintime + self.config.end_pad,
                level=level)
            return inv
        except FDSNException:
            return None
コード例 #14
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def obs_waveform_get(self, code):
        """
        Call for ObsPy FDSN webservice client to download waveform data.

        .. Note:
            ObsPy sometimes returns traces with varying sample lengths,
            so we use a 10 second cushion on start and end time and trim after
            retrieval to make sure traces are the same length.

        :type code: str
        :param code: Station code following SEED naming convention.
            This must be in the form NN.SSSS.LL.CCC (N=network, S=station,
            L=location, C=channel). Allows for wildcard naming. By default
            the pyatoa workflow wants three orthogonal components in the N/E/Z
            coordinate system. Example station code: NZ.OPRZ.10.HH?
        :rtype stream: obspy.core.stream.Stream
        :return stream: waveform contained in a stream
        :raises FDSNException: if no data found using Client
        """
        if not self.Client or self.config.synthetics_only:
            return None

        logger.debug(f"querying client {self.config.client}")
        net, sta, loc, cha = code.split('.')
        try:
            st = self.Client.get_waveforms(
                network=net,
                station=sta,
                location=loc,
                channel=cha,
                starttime=self.origintime - (self.config.start_pad + 10),
                endtime=self.origintime + (self.config.end_pad + 10))
            # Sometimes FDSN queries return improperly cut start and end times,
            # so we retrieve +/-10 seconds and then cut down
            st.trim(starttime=self.origintime - self.config.start_pad,
                    endtime=self.origintime + self.config.end_pad)
            return st
        except FDSNException:
            return None
コード例 #15
0
    def _format_windows(self):
        """
        .. note::
            In `pyadjoint.calculate_adjoint_source`, the window needs to be a
            list of lists, with each list containing the
            [left_window, right_window]; each window argument should be given in
            units of time (seconds). This is not in the PyAdjoint docs.

        :rtype: dict of list of lists
        :return: dictionary with key related to individual components,
            and corresponding to a list of lists containing window start and end
        """
        adjoint_windows = {}

        if self.windows is not None:
            for comp, window in self.windows.items():
                adjoint_windows[comp] = []
                dt = self.st_obs.select(component=comp)[0].stats.delta
                # Prepare Pyflex window indices to give to Pyadjoint
                for win in window:
                    # Window units given in seconds
                    adj_win = [win.left * dt, win.right * dt]
                    adjoint_windows[comp].append(adj_win)
        # If no windows given, calculate adjoint source on whole trace
        else:
            logger.debug("no windows given, adjoint sources will be "
                         "calculated on full trace")
            for comp in self.config.component_list:
                dt = self.st_obs.select(component=comp)[0].stats.delta
                npts = self.st_obs.select(component=comp)[0].stats.npts
                # We offset the bounds of the entire trace by 1s to play nice
                # with PyAdjoints quirky method of generating the adjsrc.
                # The assumption being the end points will be zero anyway
                adjoint_windows[comp] = [[1, npts * dt - 1]]

        return adjoint_windows
コード例 #16
0
    def _check(self):
        """
        A series of sanity checks to make sure that the configuration parameters
        are set properly to avoid any problems throughout the workflow. Should
        normally be run after any parameters are changed to make sure that they
        are acceptable.
        """
        if self.iteration is not None:
            assert(self.iteration >= 1), "Iterations must start at 1"

        if self.step_count is not None:
            assert(self.step_count >= 0), "Step count must start from 0"

        # Check period range is acceptable
        assert(self.min_period < self.max_period), \
            "min_period must be less than max_period"

        # Check if unit output properly set, dictated by ObsPy units
        acceptable_units = ['DISP', 'VEL', 'ACC']
        assert(self.unit_output in acceptable_units), \
            f"unit_output should be in {acceptable_units}"

        # Check that paths are in the proper format, dictated by Pyatoa
        required_keys = ['synthetics', 'waveforms', 'responses', 'events']
        assert(isinstance(self.paths, dict)), "paths should be a dict"
        for key in self.paths.keys():
            assert(key in required_keys), \
                f"path keys can only be in {required_keys}"

        # Make sure that all the required keys are given in the dictionary
        for key in required_keys:
            if key not in self.paths.keys():
                self.paths[key] = []

        # Set the component list. Rotate component list if necessary
        if self.rotate_to_rtz:
            logger.debug("Components changed ZNE -> ZRT")
            if not self.component_list:
                logger.debug("Component list set to R/T/Z")
                self.component_list = ["R", "T", "Z"]
            else:
                for comp in ["N", "E"]:
                    assert(comp not in self.component_list), \
                            f"rotated component list cannot include '{comp}'"
        else:
            if not self.component_list:
                logger.debug("Component list set to E/N/Z")
                self.component_list = ["E", "N", "Z"]

        # Check that the amplitude ratio is a reasonable number
        if self.win_amp_ratio > 0:
            assert(self.win_amp_ratio < 1), \
                "window amplitude ratio should be < 1"

        # Make sure adjoint source type is formatted properly
        self.adj_src_type = format_adj_src_type(self.adj_src_type)
コード例 #17
0
    def retrieve_windows(self, iteration, step_count, return_previous):
        """
        Mid-level window selection function that retrieves windows from a 
        PyASDF Dataset, recalculates window criteria, and attaches window 
        information to Manager. No access to rejected window information.

        :type iteration: int or str
        :param iteration: retrieve windows from the given iteration
        :type step_count: int or str
        :param step_count: retrieve windows from the given step count
            in the given dataset
        :type return_previous: bool
        :param return_previous: if True: return windows from the previous
            step count in relation to the given iteration/step_count.
            if False: return windows from the given iteration/step_count
        """
        logger.info(f"retrieving windows from dataset")

        net, sta, _, _ = self.st_obs[0].get_id().split(".")
        # Function will return empty dictionary if no acceptable windows found
        windows = load_windows(ds=self.ds,
                               net=net,
                               sta=sta,
                               iteration=iteration,
                               step_count=step_count,
                               return_previous=return_previous)

        # Recalculate window criteria for new values for cc, tshift, dlnA etc...
        logger.debug("recalculating window criteria")
        for comp, windows_ in windows.items():
            try:
                d = self.st_obs.select(component=comp)[0].data
                s = self.st_syn.select(component=comp)[0].data
                for w, win in enumerate(windows_):
                    # Post the old and new values to the logger for sanity check
                    logger.debug(f"{comp}{w}_old - "
                                 f"cc:{win.max_cc_value:.2f} / "
                                 f"dt:{win.cc_shift:.1f} / "
                                 f"dlnA:{win.dlnA:.2f}")
                    win._calc_criteria(d, s)
                    logger.debug(f"{comp}{w}_new - "
                                 f"cc:{win.max_cc_value:.2f} / "
                                 f"dt:{win.cc_shift:.1f} / "
                                 f"dlnA:{win.dlnA:.2f}")
            # IndexError thrown when trying to access an empty Stream
            except IndexError:
                continue

        self.windows = windows
        self.stats.nwin = sum(len(_) for _ in self.windows.values())
コード例 #18
0
def write_adj_src_to_ascii(ds,
                           iteration,
                           step_count=None,
                           pathout=None,
                           comp_list="ZNE"):
    """
    Take AdjointSource auxiliary data from a Pyasdf dataset and write out
    the adjoint sources into ascii files with proper formatting, for input
    into PyASDF.

    .. note::
        Specfem dictates that if a station is given as an adjoint source,
        all components must be present, even if some components don't have
        any misfit windows. This function writes blank adjoint sources
        (an array of 0's) to satisfy this requirement.

    :type ds: pyasdf.ASDFDataSet
    :param ds: dataset containing adjoint sources
    :type iteration: str or int
    :param iteration: iteration number, e.g. "i00". Will be formatted so int ok.
    :type step_count: str or int
    :param step_count: step count e.g. "s00". Will be formatted so int ok.
            If NoneType, final step of the iteration will be chosen automatically.
    :type pathout: str
    :param pathout: path to write the adjoint sources to
    :type comp_list: str
    :param comp_list: component list to check when writing blank adjoint sources
        defaults to N, E, Z, but can also be e.g. R, T, Z
    """
    def write_to_ascii(f_, array):
        """
        Function used to write the ascii in the correct format.
        Columns are formatted like the ASCII outputs of Specfem, two columns
        times written as float, amplitudes written in E notation, 6 spaces
        between.

        :type f_: _io.TextIO
        :param f_: the open file to write to
        :type array: numpy.ndarray
        :param array: array of data from obspy stream
        """
        for dt, amp in array:
            if dt == 0. and amp != 0.:
                dt = 0
                adj_formatter = "{dt:>13d}      {amp:13.6E}\n"
            elif dt != 0. and amp == 0.:
                amp = 0
                adj_formatter = "{dt:13.6f}      {amp:>13d}\n"
            else:
                adj_formatter = "{dt:13.6f}      {amp:13.6E}\n"

            f_.write(adj_formatter.format(dt=dt, amp=amp))

    # Shortcuts
    adjsrcs = ds.auxiliary_data.AdjointSources[format_iter(iteration)]
    if step_count is None:
        step_count = adjsrcs.list()[-1]
    adjsrcs = adjsrcs[format_step(step_count)]
    logger.debug(f"writing adjoint sources to ascii for "
                 f"{format_iter(iteration)}{format_step(step_count)}")

    # Set the path to write the data to.
    # If no path is given, default to current working directory
    if pathout is None:
        pathout = os.path.join("./", format_event_name(ds))
    if not os.path.exists(pathout):
        os.makedirs(pathout)

    # Loop through adjoint sources and write out ascii files
    # ASDF datasets use '_' as separators but Specfem wants '.' as separators
    already_written = []
    for adj_src in adjsrcs.list():
        station = adj_src.replace('_', '.')
        fid = os.path.join(pathout, f"{station}.adj")
        with open(fid, "w") as f:
            write_to_ascii(f, adjsrcs[adj_src].data[()])

        # Write blank adjoint sources for components with no misfit windows
        for comp in list(comp_list):
            station_blank = (adj_src[:-1] + comp).replace('_', '.')
            if station_blank.replace('.', '_') not in adjsrcs.list() and \
                    station_blank not in already_written:
                # Use the same adjoint source, but set the data to zeros
                blank_adj_src = adjsrcs[adj_src].data[()]
                blank_adj_src[:, 1] = np.zeros(len(blank_adj_src[:, 1]))

                # Write out the blank adjoint source
                fid_blank = os.path.join(pathout, f"{station_blank}.adj")
                with open(fid_blank, "w") as b:
                    write_to_ascii(b, blank_adj_src)

                # Append to a list to make sure we don't write doubles
                already_written.append(station_blank)
コード例 #19
0
    def measure(self, force=False, save=True):
        """
        Measure misfit and calculate adjoint sources using PyAdjoint.

        Method for caluculating misfit set in Config, Pyadjoint expects
        standardized traces with the same spectral content, so this function
        will not run unless these flags are passed.

        Returns a dictionary of adjoint sources based on component.
        Saves resultant dictionary to a pyasdf dataset if given.

        .. note::
            Pyadjoint returns an unscaled misfit value for an entire set of
            windows. To return a "total misfit" value as defined by 
            Tape (2010) Eq. 6, the total summed misfit will need to be scaled by 
            the number of misfit windows chosen in Manager.window().

        :type force: bool
        :param force: ignore flag checks and run function, useful if e.g.
            external preprocessing is used that doesn't meet flag criteria
        :type save: bool
        :param save: save adjoint sources to ASDFDataSet
        """
        self.check()

        if self.config.adj_src_type is None:
            logger.info("adjoint source type is 'None', will not measure")
            return

        # Check that data has been filtered and standardized
        if not self.stats.standardized and not force:
            raise ManagerError("cannot measure misfit, not standardized")
        elif not (self.stats.obs_processed and self.stats.syn_processed) \
                and not force:
            raise ManagerError("cannot measure misfit, not filtered")
        elif self.stats.nwin == 0 and not force:
            raise ManagerError("cannot measure misfit, no windows recovered")
        logger.debug(f"running Pyadjoint w/ type: {self.config.adj_src_type}")

        # Create list of windows needed for Pyadjoint
        adjoint_windows = self._format_windows()

        # Run Pyadjoint to retrieve adjoint source objects
        total_misfit, adjoint_sources = 0, {}
        for comp, adj_win in adjoint_windows.items():
            try:
                adj_src = pyadjoint.calculate_adjoint_source(
                    adj_src_type=self.config.adj_src_type,
                    config=self.config.pyadjoint_config,
                    observed=self.st_obs.select(component=comp)[0],
                    synthetic=self.st_syn.select(component=comp)[0],
                    window=adj_win,
                    plot=False)

                # Re-format component name to reflect SPECFEM convention
                adj_src.component = f"{channel_code(adj_src.dt)}X{comp}"

                # Save adjoint sources in dictionary object. Sum total misfit
                adjoint_sources[comp] = adj_src
                logger.info(f"{adj_src.misfit:.3f} misfit for comp {comp}")
                total_misfit += adj_src.misfit
            except IndexError:
                continue

        # Save adjoint source internally and to dataset
        self.adjsrcs = adjoint_sources
        if save:
            self.save_adjsrcs()

        # Run check to get total misfit
        self.check()
        logger.info(f"total misfit {self.stats.misfit:.3f}")

        return self
コード例 #20
0
    def standardize(self, force=False, standardize_to="syn"):
        """
        Standardize the observed and synthetic traces in place. 
        Ensures Streams have the same starttime, endtime, sampling rate, npts.

        :type force: bool
        :param force: allow the User to force the function to run even if checks
            say that the two Streams are already standardized
        :type standardize_to: str
        :param standardize_to: allows User to set which Stream conforms to which
            by default the Observed traces should conform to the Synthetic ones
            because exports to Specfem should be controlled by the Synthetic
            sampling rate, npts, etc.
        """
        self.check()
        if not self.stats.len_obs or not self.stats.len_syn:
            raise ManagerError("cannot standardize, not enough waveform data")
        elif self.stats.standardized and not force:
            logger.info("data already standardized")
            return self
        logger.info("standardizing streams")

        # If observations starttime after synthetic, zero pad the front of obs
        dt_st = self.st_obs[0].stats.starttime - self.st_syn[0].stats.starttime
        if dt_st > 0:
            self.st_obs = zero_pad(self.st_obs,
                                   dt_st,
                                   before=True,
                                   after=False)

        # Match sampling rates
        if standardize_to == "syn":
            self.st_obs.resample(self.st_syn[0].stats.sampling_rate)
        else:
            self.st_syn.resample(self.st_obs[0].stats.sampling_rate)

        # Match start and endtimes
        self.st_obs, self.st_syn = trim_streams(st_a=self.st_obs,
                                                st_b=self.st_syn,
                                                force={
                                                    "obs": "a",
                                                    "syn": "b"
                                                }[standardize_to])

        # Match the number of samples
        self.st_obs, self.st_syn = match_npts(st_a=self.st_obs,
                                              st_b=self.st_syn,
                                              force={
                                                  "obs": "a",
                                                  "syn": "b"
                                              }[standardize_to])

        # Determine if synthetics start before the origintime
        if self.event is not None:
            self.stats.time_offset_sec = (self.st_syn[0].stats.starttime -
                                          self.event.preferred_origin().time)
            logger.debug(f"time offset is {self.stats.time_offset_sec}s")
        else:
            self.stats.time_offset_sec = 0

        self.stats.standardized = True

        return self
コード例 #21
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def fetch_event_by_dir(self,
                           event_id,
                           prefix="",
                           suffix="",
                           format_=None,
                           **kwargs):
        """
        Fetch event information via directory structure on disk. Developed to
        parse CMTSOLUTION and QUAKEML files, but theoretically accepts any 
        format that the ObsPy read_events() function will accept.

        Will search through all paths given until a matching source file found.

        .. note::
            This function will search for the following path
            /path/to/event_dir/{prefix}{event_id}{suffix}
            
            so, if e.g., searching for a CMTSOLUTION file in the current dir:
            ./CMTSOLUTION_{event_id}

            Wildcards are okay but the function will return the first match

        :type event_id: str
        :param event_id: Unique event identifier to search source file by.
            e.g., a New Zealand earthquake ID '2018p130600'. A prefix or suffix
            will be tacked onto this 
        :rtype event: obspy.core.event.Event or None
        :return event: event object if found, else None.
        :type prefix: str
        :param prefix Prefix to prepend to event id for file name searching.
            Wildcards are okay.
        :type suffix: str
        :param suffix: Suffix to append to event id for file name searching.
            Wildcards are okay.
        :type format_: str or NoneType
        :param format_: Expected format of the file to read, e.g., 'QUAKEML', 
            passed to ObsPy read_events. NoneType means read_events() will guess
        """
        # Ensure that the paths are a list so that iterating doesnt accidentally
        # try to iterate through a string.
        paths = self.config.paths["events"]
        if not isinstance(paths, list):
            paths = [paths]

        event = None
        for path_ in paths:
            if not os.path.exists(path_):
                continue
            # Search for available event files
            fid = os.path.join(path_, f"{prefix}{event_id}{suffix}")
            for filepath in glob.glob(fid):
                logger.debug(f"searching for event data: {filepath}")
                if os.path.exists(filepath):
                    try:
                        # Allow input of various types of source files
                        if "SOURCE" in prefix:
                            logger.info(
                                f"reading SPECFEM2D SOURCE: {filepath}")
                            cat = [read_specfem2d_source(filepath)]
                        elif "FORCESOLUTION" in prefix:
                            logger.info(f"reading FORCESOLUTION: {filepath}")
                            cat = [read_forcesolution(filepath)]
                        else:
                            logger.info(
                                f"reading source using ObsPy: {filepath}")
                            cat = read_events(filepath, format=format_)

                        if len(cat) != 1:
                            logger.warning(
                                f"{filepath} event file contains more than one "
                                "event, returning 1st entry")
                        event = cat[0]
                        break
                    except Exception as e:
                        logger.warning(f"{filepath} event file read error {e}")

        if event is not None:
            logger.info(f"retrieved local file:\n{filepath}")
        else:
            logger.info(f"no local event file found")

        return event
コード例 #22
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def fetch_resp_by_dir(self, code, **kwargs):
        """
        Fetch station dataless via directory structure on disk.
        Will search through all paths given until StationXML found.

        .. note::
            Default path naming follows SEED convention, that is:
            path/to/dataless/{NET}.{STA}/RESP.{NET}.{STA}.{LOC}.{CHA}
            e.g. path/to/dataless/NZ.BFZ/RESP.NZ.BFZ.10.HHZ

        :type code: str
        :param code: Station code following SEED naming convention.
            This must be in the form NN.SSSS.LL.CCC (N=network, S=station,
            L=location, C=channel). Allows for wildcard naming. By default
            the pyatoa workflow wants three orthogonal components in the N/E/Z
            coordinate system. Example station code: NZ.OPRZ.10.HH?
        :rtype inv: obspy.core.inventory.Inventory or None
        :return inv: inventory containing relevant network and stations

        Keyword Arguments
        ::
            str resp_dir_template:
                Directory structure template to search for response files.
                By default follows the SEED convention,
                'path/to/RESPONSE/{sta}.{net}/'
            str resp_fid_template:
                Response file naming template to search for station dataless.
                By default, follows the SEED convention
                'RESP.{net}.{sta}.{loc}.{cha}'
        """
        resp_dir_template = kwargs.get("resp_dir_template", "{sta}.{net}")
        resp_fid_template = kwargs.get("resp_fid_template",
                                       "RESP.{net}.{sta}.{loc}.{cha}")

        inv = None
        net, sta, loc, cha = code.split('.')

        # Ensure that the paths are a list so that iterating doesnt accidentally
        # try to iterate through a string.
        paths = self.config.paths["responses"]
        if not isinstance(paths, list):
            paths = [paths]

        for path_ in paths:
            if not os.path.exists(path_):
                continue
            # Attempting to instantiate an empty Inventory requires some
            # positional arguements we dont have, so don't do that
            fid = os.path.join(path_, resp_dir_template, resp_fid_template)
            fid = fid.format(net=net, sta=sta, cha=cha, loc=loc)
            logger.debug(f"searching for responses: {fid}")
            for filepath in glob.glob(fid):
                if inv is None:
                    # The first inventory becomes the main inv to return
                    inv = read_inventory(filepath)
                else:
                    # All other inventories are appended to the original
                    inv_append = read_inventory(filepath)
                    # Merge inventories to remove repeated networks
                    inv = merge_inventories(inv, inv_append)
                logger.info(f"retrieved response locally:\n{filepath}")

        return inv
コード例 #23
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def fetch_obs_by_dir(self, code, **kwargs):
        """
        Fetch observation waveforms via directory structure on disk.

        .. note::
            Default waveform directory structure assumed to follow SEED
            convention. That is:
            path/to/data/{YEAR}/{NETWORK}/{STATION}/{CHANNEL}*/{FID}
            e.g. path/to/data/2017/NZ/OPRZ/HHZ.D/NZ.OPRZ.10.HHZ.D

        :type code: str
        :param code: Station code following SEED naming convention.
            This must be in the form NN.SSSS.LL.CCC (N=network, S=station,
            L=location, C=channel). Allows for wildcard naming. By default
            the pyatoa workflow wants three orthogonal components in the N/E/Z
            coordinate system. Example station code: NZ.OPRZ.10.HH?
        :rtype stream: obspy.core.stream.Stream or None
        :return stream: stream object containing relevant waveforms, else None

        Keyword Arguments
        ::
            str obs_dir_template:
                directory structure to search for observation data.
                Follows the SEED convention:
                'path/to/obs_data/{year}/{net}/{sta}/{cha}'
            str obs_fid_template:
                File naming template to search for observation data.
                Follows the SEED convention:
                '{net}.{sta}.{loc}.{cha}*{year}.{jday:0>3}'
        """
        obs_dir_template = kwargs.get("obs_dir_template",
                                      "{year}/{net}/{sta}/{cha}*")
        obs_fid_template = kwargs.get(
            "obs_fid_template", "{net}.{sta}.{loc}.{cha}*{year}.{jday:0>3}")

        if self.origintime is None:
            raise AttributeError("'origintime' must be specified")

        net, sta, loc, cha = code.split('.')
        # If waveforms contain midnight, multiple files need to be read
        jdays = overlapping_days(origin_time=self.origintime,
                                 start_pad=self.config.start_pad,
                                 end_pad=self.config.end_pad)

        # Ensure that the paths are a list so that iterating doesnt accidentally
        # try to iterate through a string.
        paths = self.config.paths["waveforms"]
        if not isinstance(paths, list):
            paths = [paths]

        for path_ in paths:
            if not os.path.exists(path_):
                continue
            full_path = os.path.join(path_, obs_dir_template, obs_fid_template)
            pathlist = []
            for jday in jdays:
                pathlist.append(
                    full_path.format(net=net,
                                     sta=sta,
                                     cha=cha,
                                     loc=loc,
                                     jday=jday,
                                     year=self.origintime.year))
            st = Stream()
            for fid in pathlist:
                logger.debug(f"searching for observations: {fid}")
                for filepath in glob.glob(fid):
                    st += read(filepath)
                    logger.info(f"retrieved observations locally:\n{filepath}")
            if len(st) > 0:
                # Take care of gaps in data by converting to masked data
                st.merge()
                st.trim(starttime=self.origintime - self.config.start_pad,
                        endtime=self.origintime + self.config.end_pad)
                # Check if trimming retains data
                if len(st) > 0:
                    return st
                else:
                    logger.warning(
                        "data does not fit origin time +/- pad time")
                    return None
        else:
            return None
コード例 #24
0
def trim_streams(st_a, st_b, precision=1E-3, force=None):
    """
    Trim two streams to common start and end times,
    Do some basic preprocessing before trimming.
    Allows user to force one stream to conform to another.
    Assumes all traces in a stream have the same time.
    Prechecks make sure that the streams are actually different

    :type st_a: obspy.stream.Stream
    :param st_a: streams to be trimmed
    :type st_b: obspy.stream.Stream
    :param st_b: streams to be trimmed
    :type precision: float
    :param precision: precision to check UTCDateTime differences
    :type force: str
    :param force: "a" or "b"; force trim to the length of "st_a" or to "st_b",
        if not given, trims to the common time
    :rtype: tuple of obspy.stream.Stream
    :return: trimmed stream objects in the same order as input
    """
    # Check if the times are already the same
    if st_a[0].stats.starttime - st_b[0].stats.starttime < precision and \
            st_a[0].stats.endtime - st_b[0].stats.endtime < precision:
        logger.debug(f"start and endtimes already match to {precision}")
        return st_a, st_b

    # Force the trim to the start and end times of one of the streams
    if force:
        if force.lower() == "a":
            start_set = st_a[0].stats.starttime
            end_set = st_a[0].stats.endtime
        elif force.lower() == "b":
            start_set = st_b[0].stats.starttime
            end_set = st_b[0].stats.endtime
    # Get starttime and endtime base on min values
    else:
        st_trimmed = st_a + st_b
        start_set, end_set = 0, 1E10
        for st in st_trimmed:
            start_hold = st.stats.starttime
            end_hold = st.stats.endtime
            if start_hold > start_set:
                start_set = start_hold
            if end_hold < end_set:
                end_set = end_hold

    # Trim to common start and end times
    st_a_out = st_a.copy()
    st_b_out = st_b.copy()
    for st in [st_a_out, st_b_out]:
        st.trim(start_set, end_set)

    # Trimming doesn't always make the starttimes exactly equal if the precision
    # of the UTCDateTime object is set too high.
    # Artificially shift the starttime of the streams iff the amount shifted
    # is less than the sampling rate
    for st in [st_a_out, st_b_out]:
        for tr in st:
            dt = start_set - tr.stats.starttime
            if 0 < dt < tr.stats.sampling_rate:
                logger.debug(f"shifting {tr.id} starttime by {dt}s")
                tr.stats.starttime = start_set
            elif dt >= tr.stats.delta:
                logger.warning(
                    f"{tr.id} starttime is {dt}s greater than delta")

    return st_a_out, st_b_out
コード例 #25
0
ファイル: gatherer.py プロジェクト: bch0w/pyatoa
    def fetch_syn_by_dir(self, code, **kwargs):
        """
        Fetch synthetic waveforms from Specfem3D via directory structure on
        disk, if necessary convert native ASCII format to Stream object.

        :type code: str
        :param code: Station code following SEED naming convention.
            This must be in the form NN.SSSS.LL.CCC (N=network, S=station,
            L=location, C=channel). Allows for wildcard naming. By default
            the pyatoa workflow wants three orthogonal components in the N/E/Z
            coordinate system. Example station code: NZ.OPRZ.10.HH?
        :rtype stream: obspy.core.stream.Stream or None
        :return stream: stream object containing relevant waveforms

        Keyword Arguments
        ::
            str syn_pathname:
                Config.paths key to search for synthetic data. Defaults to
                'synthetics', but for the may need to be set to 'waveforms'
                in certain use-cases.
            str syn_unit:
                Optional argument to specify the letter used to identify the
                units of the synthetic data: For Specfem3D: ["d", "v", "a", "?"]
                'd' for displacement, 'v' for velocity,  'a' for acceleration.
                Wildcards okay. Defaults to '?'
            str syn_dir_template:
                Directory structure template to search for synthetic waveforms.
                Defaults to empty string
            str syn_fid_template:
                The naming template of synthetic waveforms defaults to
                "{net}.{sta}.*{cmp}.sem{syn_unit}"
        """
        syn_cfgpath = kwargs.get("syn_cfgpath", "synthetics")
        syn_unit = kwargs.get("syn_unit", "?")
        syn_dir_template = kwargs.get("syn_dir_template", "")
        syn_fid_template = kwargs.get("syn_fid_template",
                                      "{net}.{sta}.*{cmp}.sem{dva}")

        if self.origintime is None:
            raise AttributeError("'origintime' must be specified")

        # Generate information necessary to search for data
        net, sta, loc, cha = code.split('.')

        # Ensure that the paths are a list so that iterating doesnt accidentally
        # try to iterate through a string.
        paths = self.config.paths[syn_cfgpath]
        if not isinstance(paths, list):
            paths = [paths]

        for path_ in paths:
            if not os.path.exists(path_):
                continue

            # Here the path is determined for search. If event_id is given,
            # the function will search for an event_id directory.
            full_path = os.path.join(path_, syn_dir_template, syn_fid_template)
            logger.debug(f"searching for synthetics: {full_path}")
            st = Stream()
            for filepath in glob.glob(
                    full_path.format(net=net,
                                     sta=sta,
                                     cmp=cha[2:],
                                     dva=syn_unit.lower())):
                try:
                    # Convert the ASCII file to a miniseed
                    st += read_sem(filepath, self.origintime)
                except UnicodeDecodeError:
                    # If the data file is for some reason already in miniseed
                    st += read(filepath)
                logger.info(f"retrieved synthetics locally:\n{filepath}")
            if len(st) > 0:
                st.merge()
                st.trim(starttime=self.origintime - self.config.start_pad,
                        endtime=self.origintime + self.config.end_pad)
                return st
        else:
            return None
コード例 #26
0
def filters(st,
            min_period=None,
            max_period=None,
            min_freq=None,
            max_freq=None,
            corners=2,
            zerophase=True,
            **kwargs):
    """
    Choose the appropriate filter depending on the ranges given.
    Either periods or frequencies can be given. Periods will be prioritized.
    Uses Butterworth filters by default.

    Filters the stream in place. Kwargs passed to filter functions.

    :type st: obspy.core.stream.Stream
    :param st: stream object to be filtered
    :type min_period: float
    :param min_period: minimum filter bound in units of seconds
    :type max_period: float
    :param max_period: maximum filter bound in units of seconds
    :type min_freq: float
    :param min_freq: optional minimum filter bound in units of Hz, will be
        overwritten by `max_period` if given
    :type max_freq: float
    :param max_freq: optional maximum filter bound in units of Hz, will be
        overwritten by `min_period` if given
    :type corners: int
    :param corners: number of filter corners to be passed to ObsPy
        filter functions
    :type zerophase: bool
    :param zerophase: if True, run filter backwards and forwards to avoid
        any phase shifting
    :rtype: obspy.core.stream.Stream
    :return: Filtered stream object
    """
    # Ensure that the frequency and period bounds are the same
    if not min_period and max_freq:
        min_period = 1 / max_freq
    if not max_period and min_freq:
        max_period = 1 / min_freq
    if not max_freq:
        max_freq = 1 / min_period
    if not min_freq:
        min_freq = 1 / max_period

    # Bandpass if both bounds given
    if min_period and max_period:
        st.filter("bandpass",
                  corners=corners,
                  zerophase=zerophase,
                  freqmin=min_freq,
                  freqmax=max_freq,
                  **kwargs)
        logger.debug(f"bandpass filter: {min_period} - {max_period}s w/ "
                     f"{corners} corners")

    # Highpass if only minimum period given
    elif min_period:
        st.filter("highpass",
                  freq=max_freq,
                  corners=corners,
                  zerophase=zerophase,
                  **kwargs)
        logger.debug(f"highpass filter: {min_period}s w/ {corners} corners")

    # Lowpass if only minimum period given
    elif max_period:
        st.filter("lowpass",
                  freq=min_freq,
                  corners=corners,
                  zerophase=True,
                  **kwargs)
        logger.debug(f"lowpass filter: {max_period}s w/ {corners} corners")

    return st
コード例 #27
0
def default_process(mgmt, choice, **kwargs):
    """
    Default preprocessing function to process  waveform data from a Manager
    Preprocessing is slightly different for obs and syn waveforms. Each
    processing function is split into a separate function so that they can
    be called by custom preprocessing functions.

    :type mgmt: pyatoa.core.manager.Manager
    :param mgmt: Manager class that should contain a Config object as well as
        waveform data and inventory
    :type choice: str
    :param choice: option to preprocess observed, synthetic or both
        available: 'obs', 'syn'
    :rtype: obspy.core.stream.Stream
    :return: preprocessed stream object pertaining to `choice`

    Keyword Arguments
    ::
        int water_level:
            water level for response removal
        float taper_percentage:
            amount to taper ends of waveform
        bool remove_response:
            remove instrument response using the Manager's inventory object.
            Defaults to True
        bool apply_filter:
            filter the waveforms using the Config's min_period and max_period
            parameters. Defaults to True
        bool convolve_with_stf:
            Convolve synthetic data with a Gaussian source time function if a
            half duration is provided.
    """
    assert choice in ["obs", "syn"], "choice must be 'obs' or 'syn"

    water_level = kwargs.get("water_level", 60)
    taper_percentage = kwargs.get("taper_percentage", 0.05)
    zerophase = kwargs.get("zerophase", True)
    remove_response = kwargs.get("remove_response", True)
    apply_filter = kwargs.get("apply_filter", True)
    convolve_with_stf = kwargs.get("convolve_with_stf", True)

    # Copy the stream to avoid editing in place. Synthetic variable used to
    # denote if the waveforms are synthetic or not, these require special
    # processing steps.
    if choice == "syn":
        st = mgmt.st_syn.copy()
        is_synthetic_data = True
    elif choice == "obs":
        st = mgmt.st_obs.copy()
        is_synthetic_data = mgmt.config.synthetics_only

    if is_preprocessed(st):
        return st

    # Get rid of any long period trends that may affect that data
    st.detrend("simple").detrend("demean").taper(taper_percentage)
    st = taper_time_offset(st, taper_percentage, mgmt.stats.time_offset_sec)

    # Observed specific data preprocessing includes response and rotating to ZNE
    if remove_response and not is_synthetic_data:
        logger.debug(f"removing response, units to {mgmt.config.unit_output}")
        st.remove_response(inventory=mgmt.inv,
                           output=mgmt.config.unit_output,
                           water_level=water_level,
                           plot=False)

        # Rotate streams if not in ZNE, e.g. Z12. Only necessary for observed
        logger.debug("rotating from generic coordinate system to ZNE")
        st.rotate(method="->ZNE", inventory=mgmt.inv)
        st.detrend("simple").detrend("demean").taper(taper_percentage)
    else:
        logger.debug("no response removal, synthetic data or requested not to")

    # Rotate the given stream from standard NEZ to RTZ if BAz given
    if mgmt.baz:
        logger.debug(f"rotating NE->RT by {mgmt.baz} degrees")
        st.rotate(method="NE->RT", back_azimuth=mgmt.baz)

    # Filter data based on the given period bounds
    if apply_filter:
        st = filters(st,
                     min_period=mgmt.config.min_period,
                     max_period=mgmt.config.max_period,
                     corners=mgmt.config.filter_corners,
                     zerophase=zerophase)
        st.detrend("simple").detrend("demean").taper(taper_percentage)
    else:
        logger.debug(f"no filter applied to data")

    # Convolve synthetic data with a Gaussian source time function
    if convolve_with_stf and is_synthetic_data and mgmt.stats.half_dur:
        st = stf_convolve(st=st, half_duration=mgmt.stats.half_dur)

    return st
コード例 #28
0
ファイル: gather.py プロジェクト: bch0w/pyatoa
def geonet_mt(event_id, units, event=None, csv_fid=None):
    """
    Focal mechanisms created by John Ristau are written to a .csv file
    located on Github. This function will append information from the .csv file
    onto the Obspy event object so that all the information can be located in a
    single object

    :type event_id: str
    :param event_id: unique event identifier
    :type units: str
    :param units: output units of the focal mechanism, either: 
        'dynecm': for dyne*cm  or 
        'nm': for Newton*meter
    :type event: obspy.core.event.Event
    :param event: event to append focal mechanism to
    :rtype focal_mechanism: obspy.core.event.FocalMechanism
    :return focal_mechanism: generated focal mechanism
    """
    assert (units in ["dynecm", "nm"]), "units must be 'dynecm' or 'nm'"

    mtlist = get_geonet_mt(event_id, csv_fid=csv_fid)

    # Match the identifier with Goenet
    id_template = f"smi:local/geonetcsv/{mtlist['PublicID']}/{{}}"

    # Generate the Nodal Plane objects containing strike-dip-rake
    nodal_plane_1 = source.NodalPlane(strike=mtlist['strike1'],
                                      dip=mtlist['dip1'],
                                      rake=mtlist['rake1'])
    nodal_plane_2 = source.NodalPlane(strike=mtlist['strike2'],
                                      dip=mtlist['dip2'],
                                      rake=mtlist['rake2'])
    nodal_planes = source.NodalPlanes(nodal_plane_1,
                                      nodal_plane_2,
                                      preferred_plane=1)

    # Create the Principal Axes as Axis objects
    tension_axis = source.Axis(azimuth=mtlist['Taz'],
                               plunge=mtlist['Tpl'],
                               length=mtlist['Tva'])
    null_axis = source.Axis(azimuth=mtlist['Naz'],
                            plunge=mtlist['Npl'],
                            length=mtlist['Nva'])
    pressure_axis = source.Axis(azimuth=mtlist['Paz'],
                                plunge=mtlist['Ppl'],
                                length=mtlist['Pva'])
    principal_axes = source.PrincipalAxes(t_axis=tension_axis,
                                          p_axis=pressure_axis,
                                          n_axis=null_axis)

    # Create the Moment Tensor object with correct units and scaling
    if units == "nm":
        c = 1E-7  # conversion from dyne*cm to N*m
        logger.debug(f"GeoNet moment tensor is in units of Newton*meters")
    elif units == "dynecm":
        c = 1
        logger.debug(f"GeoNet moment tensor is in units of dyne*cm")

    # CV is the conversion from non-units to the desired output units
    cv = 1E20 * c
    seismic_moment_in_nm = mtlist['Mo'] * c

    # Convert the XYZ coordinate system of GeoNet to an RTP coordinate system
    # expected in the CMTSOLUTION file of Specfem
    rtp = mt_transform(mt={
        "m_xx": mtlist['Mxx'] * cv,
        "m_yy": mtlist['Myy'] * cv,
        "m_zz": mtlist['Mzz'] * cv,
        "m_xy": mtlist['Mxy'] * cv,
        "m_xz": mtlist['Mxz'] * cv,
        "m_yz": mtlist['Myz'] * cv
    },
                       method="xyz2rtp")
    tensor = source.Tensor(m_rr=rtp['m_rr'],
                           m_tt=rtp['m_tt'],
                           m_pp=rtp['m_pp'],
                           m_rt=rtp['m_rt'],
                           m_rp=rtp['m_rp'],
                           m_tp=rtp['m_tp'])
    # Create the source time function
    source_time_function = source.SourceTimeFunction(
        duration=2 * half_duration_from_m0(seismic_moment_in_nm))

    # Generate a comment for provenance
    comment = Comment(
        force_resource_id=True,
        text="Automatically generated by Pyatoa via GeoNet MT CSV")

    # Fill the moment tensor object
    moment_tensor = source.MomentTensor(
        force_resource_id=True,
        tensor=tensor,
        source_time_function=source_time_function,
        # !!!
        # This doesn't play nice with obspy.Catalog.write(format='CMTSOLUTION')
        # so ignore the origin id
        # derived_origin_id=id_template.format('origin#ristau'),
        scalar_moment=seismic_moment_in_nm,
        double_couple=mtlist['DC'] / 100,
        variance_reduction=mtlist['VR'],
        comment=comment)

    # Finally, assemble the Focal Mechanism. Force a resource id so that
    # the event can identify its preferred focal mechanism
    focal_mechanism = source.FocalMechanism(force_resource_id=True,
                                            nodal_planes=nodal_planes,
                                            moment_tensor=moment_tensor,
                                            principal_axes=principal_axes,
                                            comments=[comment])

    # Append the focal mechanisms to the event object. Set the preferred
    # focal mechanism so that this attribute can be used in the future
    if event:
        event.focal_mechanisms = [focal_mechanism]
        event.preferred_focal_mechanism_id = focal_mechanism.resource_id
        return event, focal_mechanism
    # If no event is given, just return the focal mechanism
    else:
        return None, focal_mechanism