Ejemplo n.º 1
0
 def _validate_config_file(self):
     """
     Check to make sure the inputs into the project are compatible
     """
     stf = self.simulation_settings["source_time_function"]
     misfit = self.optimization_settings["misfit_type"]
     if stf not in ("heaviside", "bandpass_filtered_heaviside"):
         raise LASIFError(
             f" \n\nSource time function {stf} is not "
             f"supported by Lasif. \n"
             f'The only supported STF\'s are "heaviside" '
             f'and "bandpass_filtered_heaviside". \n'
             f"Please modify your config file."
         )
     if misfit not in (
         "tf_phase_misfit",
         "waveform_misfit",
         "cc_traveltime_misfit",
         "cc_traveltime_misfit_Korta2018",
         "weighted_waveform_misfit",
     ):
         raise LASIFError(
             f"\n\nMisfit type {misfit} is not supported "
             f"by LASIF. \n"
             f"Currently the only supported misfit type"
             f" is:\n "
             f'"tf_phase_misfit" ,'
             f'\n "cc_traveltime_misfit", '
             f'\n "waveform_misfit" and '
             f'\n "cc_traveltime_misfit_Korta2018".'
         )
Ejemplo n.º 2
0
    def __init__(self, points: np.array, outside: tuple = None):
        """
        Definition of polygon. Contains boundary points and potentially a
        point which is inside the domain

        :param points: Edge coordinates on unit sphere. Nx3 array of x,y,z
        :type points: numpy.ndarray
        :param outside: (x,y,z) triple of an outside coordinate,
            defaults to None
        :type outside: tuple, optional
        """
        if len(points) == 0:
            raise LASIFError("We do not define a polygon of no edge points")
        if not points.shape[1] == 3:
            raise LASIFError("Points should be of Nx3 dimension")

        if not np.array_equal(points[0], points[-1]):
            # We want points to be a closed loop of points
            points = np.concatenate((points, points[0]))

        if points.shape[0] < 4:
            raise LASIFError(
                "A spherical polygon should be defined by at least 3 points")

        self._points = points
        if not self.is_clockwise():
            self._points = points[::-1]

        self._outside = np.asanyarray(outside)
Ejemplo n.º 3
0
    def find_outside_point(self) -> tuple:
        """
        Find a point which is not inside the domain

        :return: Points in normalized x, y, z coordinates
        :rtype: tuple
        """
        found_latitude = False
        found_longitude = False
        if self.max_lat < 80.0:
            outside_lat = self.max_lat + 8.0
            found_latitude = True
        elif self.min_lat > -80.0:
            outside_lat = self.min_lat - 8.0
            found_latitude = True
        if self.max_lon < 170.0:
            outside_lon = self.max_lon + 8.0
            found_longitude = True
        elif self.min_lon > -170.0:
            outside_lon = self.min_lon - 8.0
            found_longitude = True

        if found_latitude and not found_longitude:
            # We can assign a random longitude as it is outside the latitudes
            outside_lon = 0.0
            found_longitude = True
        elif found_longitude and not found_latitude:
            # We can assign a random latitude as it is outside the longitudes
            outside_lat = 0.0
            found_latitude = True
        if not found_latitude and not found_longitude:
            # I might want to give the option of providing a point
            raise LASIFError("Could not find an outside point")
        return lat_lon_radius_to_xyz(outside_lat, outside_lon, 1.0)
Ejemplo n.º 4
0
    def get_misfit_for_event(
        self,
        event: str,
        iteration: str,
        weight_set_name: str = None,
        include_station_misfit: bool = False,
    ):
        """
        This function returns the total misfit for an event.

        :param event: name of the event
        :type event: str
        :param iteration: iteration for which to get the misfit
        :type iteration: str
        :param weight_set_name: Name of station weights, defaults to None
        :type weight_set_name: str, optional
        :param include_station_misfit: Whether individual station misfits
            should be written down or not, defaults to False
        :type include_station_misfit: bool, optional
        """
        misfit_file = self.get_misfit_file(iteration)
        misfits = toml.load(misfit_file)
        if event not in misfits.keys():
            raise LASIFError(
                f"Misfit has not been computed for event {event}, "
                f"iteration: {iteration}. ")
        event_misfit = misfits[event]["event_misfit"]
        if include_station_misfit:
            return misfits[event]
        else:
            return event_misfit
Ejemplo n.º 5
0
def _get_job_dict(comm: object, iteration: str, sim_type: str) -> dict:
    """
    Get dictionary with the job names
    """
    if sim_type not in ["forward", "adjoint"]:
        raise LASIFError("sim_type can only be forward or adjoint")
    iteration = comm.iterations.get_long_iteration_name(iteration)
    toml_file = (comm.project.paths["salvus_files"] / iteration /
                 f"{sim_type}_jobs.toml")

    if not os.path.exists(toml_file):
        raise LASIFError(f"Path {toml_file} does not exist")

    job_dict = toml.load(toml_file)

    return job_dict
Ejemplo n.º 6
0
def _discover_adjoint_sources():
    """
    Discovers the available adjoint sources. This should work no matter if
    lasif is checked out from git, packaged as .egg or for any other
    possibility.
    """
    from lasif.tools.adjoint import adjoint_source_types

    AdjointSource._ad_srcs = {}

    FCT_NAME = "calculate_adjoint_source"
    NAME_ATTR = "VERBOSE_NAME"
    DESC_ATTR = "DESCRIPTION"
    ADD_ATTR = "ADDITIONAL_PARAMETERS"

    path = os.path.join(
        os.path.dirname(inspect.getfile(inspect.currentframe())),
        "adjoint_source_types",
    )
    for importer, modname, _ in pkgutil.iter_modules(
        [path], prefix=adjoint_source_types.__name__ + "."):
        m = importer.find_module(modname).load_module(modname)
        if not hasattr(m, FCT_NAME):
            continue
        fct = getattr(m, FCT_NAME)
        if not callable(fct):
            continue

        name = modname.split(".")[-1]

        if not hasattr(m, NAME_ATTR):
            raise LASIFError(
                "Adjoint source '%s' does not have a variable named %s." %
                (name, NAME_ATTR))

        if not hasattr(m, DESC_ATTR):
            raise LASIFError(
                "Adjoint source '%s' does not have a variable named %s." %
                (name, DESC_ATTR))

        # Add tuple of name, verbose name, and description.
        AdjointSource._ad_srcs[name] = (
            fct,
            getattr(m, NAME_ATTR),
            getattr(m, DESC_ATTR),
            getattr(m, ADD_ATTR) if hasattr(m, ADD_ATTR) else None,
        )
Ejemplo n.º 7
0
    def parse(obj: object, network_code: str = None):
        """
        Based on the receiver parser of Salvus Seismo by Lion Krischer and
        Martin van Driel.
        It aims to parse an obspy inventory object into a list of
        Receiver objects
        Maybe we want to remove this elliptic to geocentric latitude thing
        at some point. Depends on what solver wants.

        :param obj: Obspy inventory object
        :type obj: object
        :param network_code: Used to keep information about network when
            at the station level, defaults to None
        :type network_code: str, optional
        """
        receivers = []

        if isinstance(obj, obspy.core.inventory.Inventory):
            for network in obj:
                receivers.extend(Receiver.parse(network))
            return receivers

        elif isinstance(obj, obspy.core.inventory.Network):
            for station in obj:
                receivers.extend(Receiver.parse(station,
                                                network_code=obj.code))
            return receivers

        elif isinstance(obj, obspy.core.inventory.Station):
            # If there are no channels, use the station coordinates
            if not obj.channels:
                return [
                    Receiver(
                        latitude=elliptic_to_geocentric_latitude(obj.latitude),
                        longitude=obj.longitude,
                        network=network_code,
                        station=obj.code,
                    )
                ]
            # Otherwise we use channel information
            else:
                coords = set((_i.latitude, _i.longitude, _i.depth)
                             for _i in obj.channels)
                if len(coords) != 1:
                    raise LASIFError(
                        f"Coordinates of channels of station "
                        f"{network_code}.{obj.code} are not identical")
                coords = coords.pop()
                return [
                    Receiver(
                        latitude=elliptic_to_geocentric_latitude(coords[0]),
                        longitude=coords[1],
                        depth_in_m=coords[2],
                        network=network_code,
                        station=obj.code,
                    )
                ]
Ejemplo n.º 8
0
    def get_project_function(self, fct_type: str):
        """
        Helper importing the project specific function.

        :param fct_type: The desired function.
        :type fct_type: str
        """
        # Cache to avoid repeated imports.
        if fct_type in self.__project_function_cache:
            return self.__project_function_cache[fct_type]

        # type / filename map
        fct_type_map = {
            "window_picking_function": "window_picking_function.py",
            "processing_function": "process_data.py",
            "preprocessing_function_asdf": "preprocessing_function_asdf.py",
            "process_synthetics": "process_synthetics.py",
            "source_time_function": "source_time_function.py",
            "light_preprocessing_function": "light_preprocessing.py",
        }

        if fct_type not in fct_type:
            msg = "Function '%s' not found. Available types: %s" % (
                fct_type,
                str(list(fct_type_map.keys())),
            )
            raise LASIFNotFoundError(msg)

        filename = os.path.join(
            self.paths["functions"], fct_type_map[fct_type]
        )
        if not os.path.exists(filename):
            msg = "No file '%s' in existence." % filename
            raise LASIFNotFoundError(msg)
        fct_template = importlib.machinery.SourceFileLoader(
            "_lasif_fct_template", filename
        ).load_module("_lasif_fct_template")

        try:
            fct = getattr(fct_template, fct_type)
        except AttributeError:
            raise LASIFNotFoundError(
                "Could not find function %s in file '%s'"
                % (fct_type, filename)
            )

        if not callable(fct):
            raise LASIFError(
                "Attribute %s in file '%s' is not a function."
                % (fct_type, filename)
            )

        # Add to cache.
        self.__project_function_cache[fct_type] = fct
        return fct
Ejemplo n.º 9
0
def list_events(
    lasif_root,
    just_list: bool = True,
    iteration: str = None,
    output: bool = True,
):
    """
    Print a list of events in project

    :param lasif_root: path to lasif root directory
    :type lasif_root: Union[str, pathlib.Path, object]
    :param just_list: Show only a plain list of events, if False it gives
        more information on the events, defaults to True
    :type just_list: bool, optional
    :param iteration: Show only events for specific iteration, defaults to None
    :type iteration: str, optional
    :param output: Do you want to output the list into a variable, defaults
        to True
    :type output: bool, optional
    """

    comm = find_project_comm(lasif_root)
    if just_list:
        if output:
            return comm.events.list(iteration=iteration)
        else:
            for event in sorted(comm.events.list(iteration=iteration)):
                print(event)

    else:
        if output:
            raise LASIFError("You can only output a basic list")
        print("%i event%s in %s:" % (
            comm.events.count(),
            "s" if comm.events.count() != 1 else "",
            "iteration" if iteration else "project",
        ))

        from lasif.tools.prettytable import PrettyTable

        tab = PrettyTable(["Event Name", "Lat", "Lng", "Depth (km)", "Mag"])

        for event in comm.events.list(iteration=iteration):
            ev = comm.events.get(event)
            tab.add_row([
                event,
                "%6.1f" % ev["latitude"],
                "%6.1f" % ev["longitude"],
                "%3i" % int(ev["depth_in_km"]),
                "%3.1f" % ev["magnitude"],
            ])
        tab.align = "r"
        tab.align["Event Name"] = "l"
        print(tab)
Ejemplo n.º 10
0
def create_salvus_simulation(
    lasif_root: Union[str, object],
    event: str,
    iteration: str,
    mesh: Union[str, pathlib.Path, object] = None,
    side_set: str = None,
    type_of_simulation: str = "forward",
):
    """
    Create a Salvus simulation object based on simulation and salvus
    specific parameters specified in config file.

    :param lasif_root: path to lasif root folder or the lasif communicator
        object
    :type lasif_root: Union[str, pathlib.Path, object]
    :param event: Name of event
    :type event: str
    :param iteration: Name of iteration
    :type iteration: str
    :param mesh: Path to mesh or Salvus mesh object, if None it will use
        the domain file from config file, defaults to None
    :type mesh: Union[str, pathlib.Path, object], optional
    :param side_set: Name of side set on mesh to place receivers,
        defaults to None.
    :type side_set: str, optional
    :param type_of_simulation: forward or adjoint, defaults to forward
    :type type_of_simulation: str, optional
    :return: Salvus simulation object
    :rtype: object
    """
    if type_of_simulation == "forward":
        from lasif.salvus_utils import create_salvus_forward_simulation as css
    elif type_of_simulation == "adjoint":
        from lasif.salvus_utils import create_salvus_adjoint_simulation as css
    else:
        raise LASIFError("Only types of simulations are forward or adjoint")

    comm = find_project_comm(lasif_root)
    if type_of_simulation == "forward":
        return css(
            comm=comm,
            event=event,
            iteration=iteration,
            mesh=mesh,
            side_set=side_set,
        )
    else:
        return css(
            comm=comm,
            event=event,
            iteration=iteration,
            mesh=mesh,
        )
Ejemplo n.º 11
0
def event_info(lasif_root, event_name: str, verbose: bool = False):
    """
    Print information about a single event

    :param lasif_root: path to lasif root directory
    :type lasif_root: Union[str, pathlib.Path, object]
    :param event_name: Name of event
    :type event_name: str
    :param verbose: Print station information as well, defaults to False
    :type verbose: bool, optional
    """

    comm = find_project_comm(lasif_root)
    if not comm.events.has_event(event_name):
        msg = "Event '%s' not found in project." % event_name
        raise LASIFError(msg)

    event_dict = comm.events.get(event_name)

    print("Earthquake with %.1f %s at %s" % (
        event_dict["magnitude"],
        event_dict["magnitude_type"],
        event_dict["region"],
    ))
    print("\tLatitude: %.3f, Longitude: %.3f, Depth: %.1f km" % (
        event_dict["latitude"],
        event_dict["longitude"],
        event_dict["depth_in_km"],
    ))
    print("\t%s UTC" % str(event_dict["origin_time"]))

    try:
        stations = comm.query.get_all_stations_for_event(event_name)
    except LASIFError:
        stations = {}

    if verbose:
        from lasif.utils import table_printer

        print("\nStation and waveform information available at %i "
              "stations:\n" % len(stations))
        header = ["ID", "Latitude", "Longitude", "Elevation_in_m"]
        keys = sorted(stations.keys())
        data = [[
            key,
            stations[key]["latitude"],
            stations[key]["longitude"],
            stations[key]["elevation_in_m"],
        ] for key in keys]
        table_printer(header, data)
    else:
        print("\nStation and waveform information available at %i stations. "
              "Use '-v' to print them." % len(stations))
Ejemplo n.º 12
0
def init_project(project_path: Union[str, pathlib.Path]):
    """
    Create a new project

    :param project_path: Path to project root directory. Can use absolute
        paths or relative paths from current working directory.
    :type project_path: Union[str, pathlib.Path]
    """

    project_path = pathlib.Path(project_path).absolute()

    if project_path.exists():
        msg = "The given PROJECT_PATH already exists. It must not exist yet."
        raise LASIFError(msg)
    try:
        os.makedirs(project_path)
    except Exception:
        msg = f"Failed creating directory {project_path}. Permissions?"
        raise LASIFError(msg)

    Project(project_root_path=project_path, init_project=project_path.name)
    print(f"Initialized project in {project_path.name}")
Ejemplo n.º 13
0
def write_custom_stf(stf_path: Union[pathlib.Path, str], comm: object):
    """
    Write the custom source-time-function specified in lasif config into
    the correct file

    :param stf_path: File path of the STF function
    :type stf_path: Union[pathlib.Path, str]
    :param comm: Lasif communicator
    :type comm: object
    """
    import h5py

    freqmax = 1.0 / comm.project.simulation_settings["minimum_period_in_s"]
    freqmin = 1.0 / comm.project.simulation_settings["maximum_period_in_s"]

    delta = comm.project.simulation_settings["time_step_in_s"]
    npts = comm.project.simulation_settings["number_of_time_steps"]

    stf_fct = comm.project.get_project_function("source_time_function")
    stf = comm.project.simulation_settings["source_time_function"]
    if stf == "bandpass_filtered_heaviside":
        stf = stf_fct(npts=npts, delta=delta, freqmin=freqmin, freqmax=freqmax)
    elif stf == "heaviside":
        stf = stf_fct(npts=npts, delta=delta)
    else:
        raise LASIFError(
            f"{stf} is not supported by lasif. Use either "
            f"bandpass_filtered_heaviside or heaviside."
        )

    stf_mat = np.zeros((len(stf), 6))
    # for i, moment in enumerate(moment_tensor):
    #     stf_mat[:, i] = stf * moment
    # Now we add the spatial weights into salvus
    for i in range(6):
        stf_mat[:, i] = stf

    heaviside_file_name = os.path.join(stf_path)
    f = h5py.File(heaviside_file_name, "w")

    source = f.create_dataset("source", data=stf_mat)
    source.attrs["dt"] = delta
    source.attrs["sampling_rate_in_hertz"] = 1 / delta
    # source.attrs["location"] = location
    source.attrs["spatial-type"] = np.string_("moment_tensor")
    # Start time in nanoseconds
    source.attrs["start_time_in_seconds"] = comm.project.simulation_settings[
        "start_time_in_s"
    ]

    f.close()
Ejemplo n.º 14
0
    def __init__(
        self, project_root_path: pathlib.Path, init_project: bool = False
    ):
        """
        Upon intialization, set the paths and read the config file.

        :param project_root_path: The root path of the project.
        :type project_root_path: pathlib.Path
        :param init_project: Determines whether or not to initialize a new
            project, e.g. create the necessary folder structure. If a string is
            passed, the project will be given this name. Otherwise a default
            name will be chosen. Defaults to False.
        :type init_project: bool, optional
        """
        # Setup the paths.
        self.__setup_paths(project_root_path.absolute())

        if init_project:
            if not project_root_path.exists():
                os.makedirs(project_root_path)
            self.__init_new_project(init_project)

        if not self.paths["config_file"].exists():
            msg = (
                "Could not find the project's config file. Wrong project "
                "path or uninitialized project?"
            )
            raise LASIFError(msg)

        # Setup the communicator and register this component.
        self.__comm = Communicator()
        super(Project, self).__init__(self.__comm, "project")

        self.__setup_components()

        # Finally update the folder structure.
        self.__update_folder_structure()

        self._read_config_file()
        self._validate_config_file()

        # Functions will be cached here.
        self.__project_function_cache = {}
        self.__copy_fct_templates(init_project=init_project)

        # Write a default window set file
        if init_project:
            default_window_filename = os.path.join(
                self.paths["windows"], "A.sqlite"
            )
            open(default_window_filename, "w").close()
Ejemplo n.º 15
0
    def plot_events(
        self,
        plot_type: str = "map",
        iteration: str = None,
        inner_boundary: bool = False,
    ):
        """
        Plots the domain and beachballs for all events on the map.

        :param plot_type: Determines the type of plot created.
            * ``map`` (default) - a map view of the events
            * ``depth`` - a depth distribution histogram
            * ``time`` - a time distribution histogram
        :type plot_type: str, optional
        :param iteration: Name of iteration, if given only events from that
            iteration will be plotted, defaults to None
        :type iteration: str, optional
        :param inner_boundary: Should we plot inner boundary of domain?
            defaults to False
        :type inner_boundary: bool, optional
        """
        from lasif import visualization

        if iteration:
            events_used = self.comm.events.list(iteration=iteration)
            events = {}
            for event in events_used:
                events[event] = self.comm.events.get(event)
            events = events.values()
        else:
            events = self.comm.events.get_all_events().values()

        if plot_type == "map":
            m, projection = self.plot_domain(inner_boundary=inner_boundary)
            visualization.plot_events(
                events,
                map_object=m,
            )
            if iteration:
                title = f"Event distribution for iteration: {iteration}"
            else:
                title = "Event distribution"
            m.set_title(title)
        elif plot_type == "depth":
            visualization.plot_event_histogram(events, "depth")
        elif plot_type == "time":
            visualization.plot_event_histogram(events, "time")
        else:
            msg = "Unknown plot_type"
            raise LASIFError(msg)
Ejemplo n.º 16
0
    def get_sorted_edge_coords(self):
        """
        Gets the indices of a sorted array of domain edge nodes,
        this method should work, as long as the top surfaces of the elements
        are approximately square
        """

        if not self.KDTrees_initialized:
            self._initialize_kd_trees()

        # For each point get the indices of the five nearest points, of
        # which the first one is the point itself.
        _, indices_nearest = self.domain_edge_tree.query(
            self.domain_edge_coords, k=5
        )
        indices_nearest = indices_nearest[:, 0, :]
        num_edge_points = len(self.domain_edge_coords)
        indices_sorted = np.zeros(num_edge_points, dtype=int)

        # start sorting with the first node
        indices_sorted[0] = 0
        for i in range(num_edge_points)[1:]:
            prev_idx = indices_sorted[i - 1]
            # take 4 closest points
            closest_indices = indices_nearest[prev_idx, 1:]
            if not closest_indices[0] in indices_sorted:
                indices_sorted[i] = closest_indices[0]
            elif not closest_indices[1] in indices_sorted:
                indices_sorted[i] = closest_indices[1]
            elif not closest_indices[2] in indices_sorted:
                indices_sorted[i] = closest_indices[2]
            elif not closest_indices[3] in indices_sorted:
                indices_sorted[i] = closest_indices[3]
            else:
                raise LASIFError(
                    "Edge node sort algorithm only works "
                    "for reasonably square elements"
                )
        return indices_sorted
Ejemplo n.º 17
0
def plot_stf(lasif_root):
    """
    Plot the source time function

    :param lasif_root: path to lasif root directory
    :type lasif_root: Union[str, pathlib.Path, object]
    """
    import lasif.visualization

    comm = find_project_comm(lasif_root)

    freqmax = 1.0 / comm.project.simulation_settings["minimum_period_in_s"]
    freqmin = 1.0 / comm.project.simulation_settings["maximum_period_in_s"]

    stf_fct = comm.project.get_project_function("source_time_function")

    delta = comm.project.simulation_settings["time_step_in_s"]
    npts = comm.project.simulation_settings["number_of_time_steps"]
    stf_type = comm.project.simulation_settings["source_time_function"]

    stf = {"delta": delta}
    if stf_type == "heaviside":
        stf["data"] = stf_fct(npts=npts, delta=delta)
        lasif.visualization.plot_heaviside(stf["data"], stf["delta"])
    elif stf_type == "bandpass_filtered_heaviside":
        stf["data"] = stf_fct(npts=npts,
                              delta=delta,
                              freqmin=freqmin,
                              freqmax=freqmax)
        lasif.visualization.plot_tf(stf["data"],
                                    stf["delta"],
                                    freqmin=freqmin,
                                    freqmax=freqmax)
    else:
        raise LASIFError(f"{stf_type} is not supported by lasif. Check your"
                         f"config file and make sure the source time "
                         f"function is either 'heaviside' or "
                         f"'bandpass_filtered_heaviside'.")
Ejemplo n.º 18
0
    def create_new_weight_set(self, weight_set_name: str, events_dict: dict):
        """
        Creates a new weight set.

        :param weight_set_name: The name of the weight set.
        :type weight_set_name: str
        :param events_dict: A dictionary specifying the used events.
        :type events_dict: dict
        """
        weight_set_name = str(weight_set_name)
        if weight_set_name in self.get_weight_set_dict():
            msg = "Weight set %s already exists." % weight_set_name
            raise LASIFError(msg)

        self.create_folder_for_weight_set(weight_set_name)

        from lasif.weights_toml import create_weight_set_toml_string

        with open(
            self.get_filename_for_weight_set(weight_set_name), "wt"
        ) as fh:
            fh.write(
                create_weight_set_toml_string(weight_set_name, events_dict)
            )
    def process_function(st, inv):
        for tr in st:
            # Trim to reduce processing costs
            tr.trim(starttime - 0.2 * duration, endtime + 0.2 * duration)

            # Decimation
            while True:
                decimation_factor = int(processing_info["dt"] / tr.stats.delta)
                # Decimate in steps for large sample rate reductions.
                if decimation_factor > 8:
                    decimation_factor = 8
                if decimation_factor > 1:
                    new_nyquist = (tr.stats.sampling_rate / 2.0 /
                                   float(decimation_factor))
                    zerophase_chebychev_lowpass_filter(tr, new_nyquist)
                    tr.decimate(factor=decimation_factor, no_filter=True)
                else:
                    break

        # Detrend and taper
        st.detrend("linear")
        st.detrend("demean")
        st.taper(max_percentage=0.05, type="hann")

        # Instrument correction
        try:
            st.attach_response(inv)
            st.remove_response(output="DISP",
                               pre_filt=pre_filt,
                               zero_mean=False,
                               taper=False)
        except Exception as e:
            net = inv.get_contents()["channels"][0].split(".", 2)[0]
            sta = inv.get_contents()["channels"][0].split(".", 2)[1]
            inf = processing_info["asdf_input_filename"]

            msg = (
                f"Station: {net}.{sta} could not be corrected with the help of"
                f" asdf file: '{inf}'. Due to: '{e.__repr__()}'  "
                f"Will be skipped.")
            raise LASIFError(msg)

        # Rotate potential BHZ,BH1,BH2 data to BHZ,BHN,BHE
        if len(st) == 3:
            for tr in st:
                if tr.stats.channel in ["BH1", "BH2"]:
                    try:
                        st._rotate_to_zne(inv)
                        break
                    except Exception as e:
                        net = inv.get_contents()["channels"][0].split(".",
                                                                      2)[0]
                        sta = inv.get_contents()["channels"][0].split(".",
                                                                      2)[1]
                        inf = processing_info["asdf_input_filename"]

                        msg = (
                            f"Station: {net}.{sta} could not be rotated with"
                            f" the help of"
                            f" asdf file: '{inf}'. Due to: '{e.__repr__()}'  "
                            f"Will be skipped.")
                        raise LASIFError(msg)

        # Bandpass filtering
        st.detrend("linear")
        st.detrend("demean")
        st.taper(0.05, type="cosine")
        st.filter(
            "bandpass",
            freqmin=1.0 / max_period,
            freqmax=1.0 / min_period,
            corners=3,
            zerophase=False,
        )

        st.detrend("linear")
        st.detrend("demean")
        st.taper(0.05, type="cosine")
        st.filter(
            "bandpass",
            freqmin=1.0 / max_period,
            freqmax=1.0 / min_period,
            corners=3,
            zerophase=False,
        )

        # Sinc interpolation
        for tr in st:
            tr.data = np.require(tr.data, requirements="C")

        st.interpolate(
            sampling_rate=sampling_rate,
            method="lanczos",
            starttime=starttime,
            window="blackman",
            a=12,
            npts=npts,
        )

        # Convert to single precision to save space.
        for tr in st:
            tr.data = np.require(tr.data, dtype="float32", requirements="C")

        return st
Ejemplo n.º 20
0
def get_subset_of_events(comm, count, events, existing_events=None):
    """
    This function gets an optimally distributed set of events,
    NO QA.
    :param comm: LASIF communicator
    :param count: number of events to choose.
    :param events: list of event_names, from which to choose from. These
    events must be known to LASIF
    :param existing_events: list of events, that have been chosen already
    and should thus be excluded from the selected options, but are also
    taken into account when ensuring a good spatial distribution. The
    function assumes that there are no common occurences between
    events and existing events
    :return: a list of chosen events.
    """
    available_events = comm.events.list()

    if len(events) < count:
        raise LASIFError("Insufficient amount of events specified.")
    if not type(count) == int:
        raise ValueError("count should be an integer value.")
    if count < 1:
        raise ValueError("count should be at least 1.")
    for event in events:
        if event not in available_events:
            raise LASIFNotFoundError(f"event : {event} not known to LASIF.")

    if existing_events is None:
        existing_events = []
    else:
        for event in events:
            if event in existing_events:
                raise LASIFError(f"event: {event} was existing already,"
                                 f"but still supplied to choose from.")

    cat = obspy.Catalog()
    for event in events:
        event_file_name = comm.waveforms.get_asdf_filename(event,
                                                           data_type="raw")
        with pyasdf.ASDFDataSet(event_file_name, mode="r") as ds:
            ev = ds.events[0]
            # append event_name to comments, such that it can later be
            # retrieved
            ev.comments.append(event)
            cat += ev

    # Coordinates and the Catalog will have the same order!
    coordinates = []
    for event in cat:
        org = event.preferred_origin() or event.origins[0]
        coordinates.append((org.latitude, org.longitude))

    chosen_events = []
    existing_coordinates = []
    for event in existing_events:
        ev = comm.events.get(event)
        existing_coordinates.append((ev["latitude"], ev["longitude"]))

    # randomly start with one of the specified events
    if not existing_coordinates:
        idx = random.randint(0, len(cat) - 1)
        chosen_events.append(cat[idx])
        del cat.events[idx]
        existing_coordinates.append(coordinates[idx])
        del coordinates[idx]
        count -= 1

    while count:
        if not coordinates:
            print("\tNo events left to select from. Stopping here.")
            break
        # Build kdtree and query for the point furthest away from any other
        # point.
        kdtree = SphericalNearestNeighbour(np.array(existing_coordinates))
        distances = kdtree.query(np.array(coordinates), k=1)[0]
        idx = np.argmax(distances)

        event = cat[idx]
        coods = coordinates[idx]
        del cat.events[idx]
        del coordinates[idx]

        chosen_events.append(event)
        existing_coordinates.append(coods)
        count -= 1

    list_of_chosen_events = []
    for ev in chosen_events:
        list_of_chosen_events.append(ev.comments.pop())
    if len(list_of_chosen_events) < count:
        raise ValueError("Could not select a sufficient amount of events")

    return list_of_chosen_events
Ejemplo n.º 21
0
def processing_function(st, inv, simulation_settings, event):  # NOQA
    """
    Function to perform the actual preprocessing for one individual seismogram.
    This is part of the project so it can change depending on the project.

    Please keep in mind that you will have to manually update this file to a
    new version if LASIF is ever updated.

    You can do whatever you want in this function as long as the function
    signature is honored. The file is read from ``"input_filename"`` and
    written to ``"output_filename"``.

    One goal of this function is to make sure that the data is available at the
    same time steps as the synthetics. The first time sample of the synthetics
    will always be the origin time of the event.

    Furthermore the data has to be converted to m/s.

    :param processing_info: A dictionary containing information about the
        file to be processed. It will have the following structure.
    :type processing_info: dict

    .. code-block:: python

        {'event_information': {
            'depth_in_km': 22.0,
            'event_name': 'GCMT_event_VANCOUVER_ISLAND...',
            'filename': '/.../GCMT_event_VANCOUVER_ISLAND....xml',
            'latitude': 49.53,
            'longitude': -126.89,
            'm_pp': 2.22e+18,
            'm_rp': -2.78e+18,
            'm_rr': -6.15e+17,
            'm_rt': 1.98e+17,
            'm_tp': 5.14e+18,
            'm_tt': -1.61e+18,
            'magnitude': 6.5,
            'magnitude_type': 'Mwc',
            'origin_time': UTCDateTime(2011, 9, 9, 19, 41, 34, 200000),
            'region': u'VANCOUVER ISLAND, CANADA REGION'},
         'input_filename': u'/.../raw/7D.FN01A..HHZ.mseed',
         'output_filename': u'/.../processed_.../7D.FN01A..HHZ.mseed',
         'process_params': {
            'dt': 0.75,
            'highpass': 0.007142857142857143,
            'lowpass': 0.0125,
            'npts': 2000},
         'station_coordinates': {
            'elevation_in_m': -54.0,
            'latitude': 46.882,
            'local_depth_in_m': None,
            'longitude': -124.3337},
         'station_filename': u'/.../STATIONS/RESP/RESP.7D.FN01A..HH*'}

    Please note that you also got the iteration object here, so if you
    want some parameters to change depending on the iteration, just use
    if/else on the iteration objects.

    >>> iteration.name  # doctest: +SKIP
    '11'
    >>> iteration.get_process_params()  # doctest: +SKIP
    {'dt': 0.75,
     'highpass': 0.01,
     'lowpass': 0.02,
     'npts': 500}

    Use ``$ lasif shell`` to play around and figure out what the iteration
    objects can do.

    """
    def zerophase_chebychev_lowpass_filter(trace, freqmax):
        """
        Custom Chebychev type two zerophase lowpass filter useful for
        decimation filtering.

        This filter is stable up to a reduction in frequency with a factor of
        10. If more reduction is desired, simply decimate in steps.

        Partly based on a filter in ObsPy.

        :param trace: The trace to be filtered.
        :param freqmax: The desired lowpass frequency.

        Will be replaced once ObsPy has a proper decimation filter.
        """
        # rp - maximum ripple of passband, rs - attenuation of stopband
        rp, rs, order = 1, 96, 1e99
        ws = freqmax / (trace.stats.sampling_rate * 0.5)  # stop band frequency
        wp = ws  # pass band frequency

        while True:
            if order <= 12:
                break
            wp *= 0.99
            order, wn = signal.cheb2ord(wp, ws, rp, rs, analog=0)

        b, a = signal.cheby2(order, rs, wn, btype="low", analog=0, output="ba")

        # Apply twice to get rid of the phase distortion.
        trace.data = signal.filtfilt(b, a, trace.data)

    # =========================================================================
    # Gather basic information.
    # =========================================================================
    npts = simulation_settings["npts"]
    dt = simulation_settings["dt"]
    min_period = simulation_settings["minimum_period"]
    max_period = simulation_settings["maximum_period"]

    starttime = event["origin_time"] + simulation_settings["start_time_in_s"]
    endtime = starttime + simulation_settings["dt"] * (
        simulation_settings["npts"] - 1)
    duration = endtime - starttime

    f2 = 0.9 / max_period
    f3 = 1.1 / min_period
    # Recommendations from the SAC manual.
    f1 = 0.5 * f2
    f4 = 2.0 * f3
    pre_filt = (f1, f2, f3, f4)

    for tr in st:
        # Make sure the seismograms are long enough. If not, skip them.
        if starttime < tr.stats.starttime or endtime > tr.stats.endtime:

            msg = ("The seismogram does not cover the required time span.\n"
                   "Seismogram time span: %s - %s\n"
                   "Requested time span: %s - %s" %
                   (tr.stats.starttime, tr.stats.endtime, starttime, endtime))
            raise LASIFError(msg)

        # Trim to reduce processing cost.
        tr.trim(starttime - 0.2 * duration, endtime + 0.2 * duration)

        # =====================================================================
        # Some basic checks on the data.
        # =====================================================================
        # Non-zero length
        if not len(tr):
            msg = ("No data found in time window around the event."
                   " File skipped.")
            raise LASIFError(msg)

        # No nans or infinity values allowed.
        if not np.isfinite(tr.data).all():
            msg = "Data contains NaNs or Infs. File skipped"
            raise LASIFError(msg)

        # =====================================================================
        # Step 1: Decimation
        # Decimate with the factor closest to the sampling rate of the
        # synthetics.
        # The data is still oversampled by a large amount so there should be no
        # problems. This has to be done here so that the instrument correction
        # is reasonably fast even for input data with a large sampling rate.
        # =====================================================================
        while True:
            decimation_factor = int(dt / tr.stats.delta)
            # Decimate in steps for large sample rate reductions.
            if decimation_factor > 8:
                decimation_factor = 8
            if decimation_factor > 1:
                new_nyquist = (tr.stats.sampling_rate / 2.0 /
                               float(decimation_factor))
                zerophase_chebychev_lowpass_filter(tr, new_nyquist)
                tr.decimate(factor=decimation_factor, no_filter=True)
            else:
                break

        # =====================================================================
        # Step 2: Detrend and taper.
        # =====================================================================
        tr.detrend("linear")
        tr.detrend("demean")
        tr.taper(max_percentage=0.05, type="hann")

        # =====================================================================
        # Step 3: Instrument correction
        # Correct seismograms to velocity in m/s.
        # ======================================================================
        try:
            tr.attach_response(inv)
            tr.remove_response(output="DISP",
                               pre_filt=pre_filt,
                               zero_mean=False,
                               taper=False)
        except Exception as e:
            station = (tr.stats.network + "." + tr.stats.station + ".." +
                       tr.stats.channel)
            msg = (("File  could not be corrected with the help of the "
                    "StationXML file '%s'. Due to: '%s'  Will be skipped.") %
                   (station, e.__repr__()), )
            raise LASIFError(msg)

        # =====================================================================
        # Step 4: Bandpass filtering
        # This has to be exactly the same filter as in the source time function
        # in the case of SES3D.
        # =====================================================================
        tr.detrend("linear")
        tr.detrend("demean")
        tr.taper(0.05, type="cosine")
        tr.filter(
            "bandpass",
            freqmin=1.0 / max_period,
            freqmax=1.0 / min_period,
            corners=3,
            zerophase=False,
        )
        tr.detrend("linear")
        tr.detrend("demean")
        tr.taper(0.05, type="cosine")
        tr.filter(
            "bandpass",
            freqmin=1.0 / max_period,
            freqmax=1.0 / min_period,
            corners=3,
            zerophase=False,
        )

        # =====================================================================
        # Step 5: Sinc interpolation
        # =====================================================================
        tr.data = np.require(tr.data, requirements="C")
        tr.interpolate(
            sampling_rate=1.0 / dt,
            method="lanczos",
            starttime=starttime,
            window="blackman",
            a=12,
            npts=npts,
        )

        # =====================================================================
        # Save processed data and clean up.
        # =====================================================================
        # Convert to single precision to save some space.
        tr.data = np.require(tr.data, dtype="float32", requirements="C")
        if hasattr(tr.stats, "mseed"):
            tr.stats.mseed.encoding = "FLOAT32"

    return st
Ejemplo n.º 22
0
def plot_stations_for_event(
    map_object,
    station_dict: Dict[str, Union[str, float]],
    event_info: Dict[str, Union[str, float]],
    color: str = "green",
    alpha: float = 1.0,
    raypaths: bool = True,
    weight_set: str = None,
    plot_misfits: bool = False,
    print_title: bool = True,
):
    """
    Plot all stations for one event

    :param map_object: Cartopy plotting object
    :type map_object: cp.mpl.geoaxes.GeoAxes
    :param station_dict: Dictionary with station information
    :type station_dict: Dict[str, Union[str, float]]
    :param event_info: Dictionary with event information
    :type event_info: Dict[str, Union[str, float]]
    :param color: Color to plot stations with, defaults to "green"
    :type color: str, optional
    :param alpha: How transparent the stations are, defaults to 1.0
    :type alpha: float, optional
    :param raypaths: Should raypaths be plotted?, defaults to True
    :type raypaths: bool, optional
    :param weight_set: Do we colorcode stations with their respective
        weights, defaults to None
    :type weight_set: str, optional
    :param plot_misfits: Color code stations with their respective
        misfits, defaults to False
    :type plot_misfits: bool, optional
    :param print_title: Have a title on the figure, defaults to True
    """
    import re

    # Check inputs:
    if weight_set and plot_misfits:
        raise LASIFError("Can't plot both weight set and misfit")

    # Loop as dicts are unordered.
    lngs = []
    lats = []
    station_ids = []

    for key, value in station_dict.items():
        lngs.append(value["longitude"])
        lats.append(value["latitude"])
        station_ids.append(key)

    event = event_info["event_name"]
    if weight_set:
        # If a weight set is specified, stations will be color coded.
        weights = []
        for id in station_ids:
            weights.append(
                weight_set.events[event]["stations"][id]["station_weight"])
        cmap = cmr.heat
        stations = map_object.scatter(
            lngs,
            lats,
            c=weights,
            cmap=cmap,
            s=35,
            marker="v",
            alpha=alpha,
            zorder=5,
            transform=cp.crs.PlateCarree(),
        )
        cbar = plt.colorbar(stations)
        cbar.ax.set_ylabel("Station Weights", rotation=-90)

    elif plot_misfits:
        misfits = [station_dict[x]["misfit"] for x in station_dict.keys()]
        cmap = cmr.heat
        # cmap = cm.get_cmap("seismic")
        stations = map_object.scatter(
            lngs,
            lats,
            c=misfits,
            cmap=cmap,
            s=35,
            marker="v",
            alpha=alpha,
            zorder=5,
            transform=cp.crs.PlateCarree(),
        )
        # from mpl_toolkits.axes_grid1 import make_axes_locatable

        # divider = make_axes_locatable(map_object)
        # cax = divider.append_axes("right", "5%", pad="3%")
        # im_ratio = map_object.shape[0] / map_object.shape[1]
        cbar = plt.colorbar(stations)
        cbar.ax.set_ylabel("Station misfits", rotation=-90)
        # plt.tight_layout()

    else:
        stations = map_object.scatter(
            lngs,
            lats,
            color=color,
            s=35,
            marker="v",
            alpha=alpha,
            zorder=5,
            transform=cp.crs.PlateCarree(),
        )
        # Setting the picker overwrites the edgecolor attribute on certain
        # matplotlib and basemap versions. Fix it here.
        stations._edgecolors = np.array([[0.0, 0.0, 0.0, 1.0]])
        stations._edgecolors_original = "black"

    # Plot the ray paths.
    if raypaths:
        for sta_lng, sta_lat in zip(lngs, lats):
            map_object.plot(
                [event_info["longitude"], sta_lng],
                [event_info["latitude"], sta_lat],
                lw=2,
                alpha=0.3,
                transform=cp.crs.PlateCarree(),
            )
    title = "Event in %s, at %s, %.1f Mw, with %i stations." % (
        event_info["region"],
        re.sub(r":\d{2}\.\d{6}Z", "", str(event_info["origin_time"])),
        event_info["magnitude"],
        len(station_dict),
    )

    weights_title = f"Event in {event_info['region']}. Station Weights"
    if print_title:
        if weight_set is not None:
            map_object.set_title(weights_title, size="large")
        elif plot_misfits:
            misfit_title = (f"Event in {event_info['region']}. "
                            f"Total misfit: '%.2f'" % (np.sum(misfits)))
            map_object.set_title(misfit_title, size="large")
        else:
            map_object.set_title(title, size="large")

    return stations
Ejemplo n.º 23
0
def create_salvus_adjoint_simulation(
    comm: object,
    event: str,
    iteration: str,
    mesh=None,
) -> object:
    """
    Create a Salvus simulation object based on simulation and salvus
    specific parameters specified in config file.

    :param comm: The lasif communicator object
    :type comm: object
    :param event: Name of event
    :type event: str
    :param iteration: Name of iteration
    :type iteration: str
    :param mesh: Path to mesh or Salvus mesh object, if None it will use
        the domain file from config file, defaults to None
    :type mesh: Union(str, salvus.mesh.unstructured_mesh.UnstructuredMesh),
        optional
    """
    import salvus.flow.api
    from salvus.flow.simple_config import simulation

    site_name = comm.project.salvus_settings["site_name"]
    forward_job_dict = _get_job_dict(
        comm=comm,
        iteration=iteration,
        sim_type="forward",
    )
    if "array_name" in forward_job_dict.keys():
        fwd_job_array = salvus.flow.api.get_job_array(
            site_name=site_name,
            job_array_name=forward_job_dict["array_name"],
        )
        fwd_job_names = [j.job_name for j in fwd_job_array.jobs]
        fwd_job_name = forward_job_dict[event]
        if fwd_job_name not in fwd_job_names:
            raise LASIFError(f"{fwd_job_name} not in job_array names")
        fwd_job_array_index = fwd_job_names.index(fwd_job_name)
        fwd_job_path = fwd_job_array.jobs[fwd_job_array_index].output_path
    else:
        fwd_job_path = salvus.flow.api.get_job(
            site_name=site_name, job_name=forward_job_dict[event]).output_path

    meta = fwd_job_path / "meta.json"

    if mesh is None:
        mesh = comm.project.lasif_config["domain_settings"]["domain_file"]

    w = simulation.Waveform(mesh=mesh)
    w.adjoint.forward_meta_json_filename = f"REMOTE:{meta}"
    w.adjoint.gradient.parameterization = comm.project.salvus_settings[
        "gradient_parameterization"]
    w.adjoint.gradient.output_filename = "gradient.h5"

    adj_src = get_adjoint_source(comm=comm, event=event, iteration=iteration)
    w.adjoint.point_source = adj_src

    w.validate()

    return w
Ejemplo n.º 24
0
def submit_salvus_simulation(
    comm: object,
    simulations: Union[List[object], object],
    events: Union[List[str], str],
    iteration: str,
    sim_type: str,
) -> object:
    """
    Submit a Salvus simulation to the machine defined in config file
    with details specified in config file

    :param comm: The Lasif communicator object
    :type comm: object
    :param simulations: Simulation object
    :type simulations: Union[List[object], object]
    :param events: We need names of events for the corresponding simulations
        in order to keep tabs on which simulation object corresponds to which
        event.
    :type events: Union[List[str], str]
    :param iteration: Name of iteration, this is needed to know where to
        download files to when jobs are done.
    :type iteration: str
    :param sim_type: can be either forward or adjoint.
    :type sim_type: str
    :return: SalvusJob object or an array of them
    :rtype: object
    """
    from salvus.flow.api import run_async, run_many_async

    if sim_type not in ["forward", "adjoint"]:
        raise LASIFError("sim_type needs to be forward or adjoint")

    array = False
    if isinstance(simulations, list):
        array = True
        if not isinstance(events, list):
            raise LASIFError(
                "If simulations are a list, events need to be "
                "a list aswell, with the corresponding events in the same "
                "order")
    else:
        if isinstance(events, list):
            raise LASIFError("If there is only one simulation object, "
                             "there should be only one event")

    iteration = comm.iterations.get_long_iteration_name(iteration)

    if sim_type == "forward":
        toml_file = (comm.project.paths["salvus_files"] / iteration /
                     "forward_jobs.toml")
    elif sim_type == "adjoint":
        toml_file = (comm.project.paths["salvus_files"] / iteration /
                     "adjoint_jobs.toml")

    if os.path.exists(toml_file):
        jobs = toml.load(toml_file)
    else:
        jobs = {}

    site_name = comm.project.salvus_settings["site_name"]
    ranks = comm.project.salvus_settings["ranks"]
    wall_time = comm.project.salvus_settings["wall_time_in_s"]

    if array:
        job = run_many_async(
            site_name=site_name,
            input_files=simulations,
            ranks_per_job=ranks,
            wall_time_in_seconds_per_job=wall_time,
        )
        jobs["array_name"] = job.job_array_name
        for _i, j in enumerate(job.jobs):
            jobs[events[_i]] = j.job_name
    else:
        job = run_async(
            site_name=site_name,
            input_file=simulations,
            ranks=ranks,
            wall_time_in_seconds=wall_time,
        )
        jobs[events] = job.job_name

    with open(toml_file, mode="w") as fh:
        toml.dump(jobs, fh)
        print(f"Wrote job information into {toml_file}")
    return job
Ejemplo n.º 25
0
def check_job_status(comm: object, events: Union[List[str], str],
                     iteration: str, sim_type: str) -> Dict[str, str]:
    """
    Check on the statuses of jobs which have been submitted before.

    :param comm: The Lasif communicator object
    :type comm: object
    :param events: We need names of events for the corresponding simulations
        in order to keep tabs on which simulation object corresponds to which
        event.
    :type events: Union[List[str], str]
    :param iteration: Name of iteration, this is needed to know where to
        download files to when jobs are done.
    :type iteration: str
    :param sim_type: can be either forward or adjoint.
    :type sim_type: str
    :return: Statuses of jobs
    :return type: Dict[str]
    """
    import salvus.flow.api

    job_dict = _get_job_dict(comm=comm, iteration=iteration, sim_type=sim_type)

    if not isinstance(events, list):
        events = [events]

    site_name = comm.project.salvus_settings["site_name"]
    statuses = {}

    if "array_name" in job_dict.keys():
        jobs = salvus.flow.api.get_job_array(
            job_array_name=job_dict["array_name"], site_name=site_name)
        jobs.update_status(force_update=True)
        job_names = [j.job_name for j in jobs.jobs]

        for event in events:
            job_name = job_dict[event]
            if job_name not in job_names:
                print(f"{job_name} not in array {job_dict['array_name']}. "
                      f"Will check to see if job was posted individually")
                job = salvus.flow.api.get_job(
                    job_name=job_name,
                    site_name=site_name,
                )
                job_updated = job.update_status(force_update=True)
                statuses[event] = job_updated
                continue
                raise LASIFError(f"{job_name} not in List of job names")
            event_job_index = job_names.index(job_name)
            event_status = jobs.jobs[event_job_index].get_status_from_db()
            statuses[event] = event_status

    else:
        for event in events:
            job_name = job_dict[event]
            job = salvus.flow.api.get_job(job_name=job_name,
                                          site_name=site_name)
            job_updated = job.update_status(force_update=True)
            statuses[event] = job_updated

    return statuses
Ejemplo n.º 26
0
    def get_matching_waveforms(
        self, event: str, iteration: str, station_or_channel_id: str
    ):
        """
        Get synthetic and processed waveforms for the same station and same
        event.

        :param event: Name of event
        :type event: str
        :param iteration: Name of iteration
        :type iteration: str
        :param station_or_channel_id: The id for the station of the channel
        :type station_or_channel_id: str
        :return: A named tuple with processed waveforms, synthetic waveforms
            and coordinates of station
        """
        seed_id = station_or_channel_id.split(".")
        if len(seed_id) == 2:
            channel = None
            station_id = station_or_channel_id
        elif len(seed_id) == 4:
            network, station, _, channel = seed_id
            station_id = ".".join((network, station))
        else:
            raise ValueError(
                "'station_or_channel_id' must either have " "2 or 4 parts."
            )

        iteration_long_name = self.comm.iterations.get_long_iteration_name(
            iteration
        )
        event = self.comm.events.get(event)

        # Get the metadata for the processed and synthetics for this
        # particular station.
        data = self.comm.waveforms.get_waveforms_processed(
            event["event_name"],
            station_id,
            tag=self.comm.waveforms.preprocessing_tag,
        )
        # data_fly = self.comm.waveforms.get_waveforms_processed_on_the_fly(
        #     event["event_name"], station_id)

        synthetics = self.comm.waveforms.get_waveforms_synthetic(
            event["event_name"],
            station_id,
            long_iteration_name=iteration_long_name,
        )
        coordinates = self.comm.query.get_coordinates_for_station(
            event["event_name"], station_id
        )

        # Clear data and synthetics!
        for _st, name in ((data, "observed"), (synthetics, "synthetic")):
            # Get all components and loop over all components.
            _comps = set(tr.stats.channel[-1].upper() for tr in _st)
            for _c in _comps:
                traces = [
                    _i for _i in _st if _i.stats.channel[-1].upper() == _c
                ]
                if len(traces) == 1:
                    continue
                elif len(traces) > 1:
                    traces = sorted(traces, key=lambda x: x.id)
                    warnings.warn(
                        "%s data for event '%s', iteration '%s', "
                        "station '%s', and component '%s' has %i traces: "
                        "%s. LASIF will select the first one, but please "
                        "clean up your data."
                        % (
                            name.capitalize(),
                            event["event_name"],
                            iteration,
                            station_id,
                            _c,
                            len(traces),
                            ", ".join(tr.id for tr in traces),
                        ),
                        LASIFWarning,
                    )
                    for tr in traces[1:]:
                        _st.remove(tr)
                else:
                    # Should not happen.
                    raise NotImplementedError

        # Make sure all data has the corresponding synthetics. It should not
        # happen that one has three channels of data but only two channels
        # of synthetics...in that case, discard the additional data and
        # raise a warning.
        temp_data = []
        for data_tr in data:
            component = data_tr.stats.channel[-1].upper()
            synthetic_tr = [
                tr
                for tr in synthetics
                if tr.stats.channel[-1].upper() == component
            ]
            if not synthetic_tr:
                warnings.warn(
                    "Station '%s' has observed data for component '%s' but no "
                    "matching synthetics." % (station_id, component),
                    LASIFWarning,
                )
                continue
            temp_data.append(data_tr)
        data.traces = temp_data

        if len(data) == 0:
            raise LASIFError(
                "No data remaining for station '%s'." % station_id
            )

        # Scale the data if required.
        if self.comm.project.simulation_settings["scale_data_to_synthetics"]:
            for data_tr in data:
                synthetic_tr = [
                    tr
                    for tr in synthetics
                    if tr.stats.channel[-1].lower()
                    == data_tr.stats.channel[-1].lower()
                ][0]
                scaling_factor = synthetic_tr.data.ptp() / data_tr.data.ptp()
                # Store and apply the scaling.
                data_tr.stats.scaling_factor = scaling_factor
                data_tr.data *= scaling_factor

        data.sort()
        synthetics.sort()

        # Select component if necessary.
        if channel and channel is not None:
            # Only use the last letter of the channel for the selection.
            # Different solvers have different conventions for the location
            # and channel codes.
            component = channel[-1].upper()
            data.traces = [
                i
                for i in data.traces
                if i.stats.channel[-1].upper() == component
            ]
            synthetics.traces = [
                i
                for i in synthetics.traces
                if i.stats.channel[-1].upper() == component
            ]

        return DataTuple(
            data=data, synthetics=synthetics, coordinates=coordinates
        )
Ejemplo n.º 27
0
def calculate_adjoint_source(
    adj_src_type,
    observed,
    synthetic,
    window,
    min_period=None,
    max_period=None,
    taper=True,
    taper_type="cosine",
    adjoint_src=True,
    plot=False,
    plot_filename=None,
    **kwargs,
):
    """
    Central function of SalvusMisfit used to calculate adjoint sources and
    misfit.

    This function uses the notion of observed and synthetic data to offer a
    nomenclature most users are familiar with. Please note that it is
    nonetheless independent of what the two data arrays actually represent.

    The function tapers the data from ``left_window_border`` to
    ``right_window_border``, both in seconds since the first sample in the
    data arrays.

    :param adj_src_type: The type of adjoint source to calculate.
    :type adj_src_type: str
    :param observed: The observed data.
    :type observed: :class:`obspy.core.trace.Trace`
    :param synthetic: The synthetic data.
    :type synthetic: :class:`obspy.core.trace.Trace`
    :param min_period: The minimum period of the spectral content of the data.
    :type min_period: float
    :param window: starttime and endtime of window(s) potentially including
        weighting for each window.
    :type window: list of tuples
    :param adjoint_src: Only calculate the misfit or also derive
        the adjoint source.
    :type adjoint_src: bool
    :param plot: Also produce a plot of the adjoint source. This will force
        the adjoint source to be calculated regardless of the value of
        ``adjoint_src``.
    :type plot: bool or empty :class:`matplotlib.figure.Figure` instance
    :param plot_filename: If given, the plot of the adjoint source will be
        saved there. Only used if ``plot`` is ``True``.
    :type plot_filename: str
    """
    observed, synthetic = _sanity_checks(observed, synthetic)
    # Keep these as they will need to be imported later

    if adj_src_type not in AdjointSource._ad_srcs:
        raise LASIFError(
            "Adjoint Source type '%s' is unknown. Available types: %s" %
            (adj_src_type, ", ".join(sorted(AdjointSource._ad_srcs.keys()))))

    # window variable should be a list of windows, if it is not make it into
    # a list.
    if not isinstance(window, list):
        window = [window]

    fct = AdjointSource._ad_srcs[adj_src_type][0]

    if plot:
        if len(window) > 1:
            raise LASIFError("Currently plotting is only implemented"
                             "for a single window.")
        adjoint_src = True

    full_ad_src = None
    trace_misfit = 0.0
    window_misfit = []
    individual_adj_srcs = Stream()
    s = 0

    original_observed = observed.copy()
    original_synthetic = synthetic.copy()

    if "envelope_scaling" in kwargs and kwargs["envelope_scaling"]:
        # normalize the trace to [-1,1], reduce source effects
        norm_scaling_fac = 1.0 / np.max(np.abs(synthetic.data))
        original_observed.data *= norm_scaling_fac
        original_synthetic.data *= norm_scaling_fac
        envelope = obspy.signal.filter.envelope(original_observed.data)
        # scale up to the noise, also never divide by 0
        env_weighting = 1.0 / (envelope + np.max(envelope) * 0.001)
        original_observed.data *= env_weighting
        original_synthetic.data *= env_weighting
    for win in window:
        taper_ratio = 0.5 * (min_period / (win[1] - win[0]))
        if taper_ratio > 0.5:
            s += 1
            station_name = (observed.stats.network + "." +
                            observed.stats.station)
            msg = (f"Window {win} at Station {station_name} might be to "
                   f"short for your frequency content. Adjoint source "
                   f"was not calculated because it could result in "
                   f"high frequency artifacts and wacky misfit measurements.")
            warnings.warn(msg)
            if len(window) == 1 or s == len(window):
                adjoint = {
                    "adjoint_source": np.zeros_like(observed.data),
                    "misfit": 0.0,
                }
            else:
                continue

        observed = original_observed.copy()
        synthetic = original_synthetic.copy()

        # The window trace function modifies the passed trace
        observed = window_trace(
            trace=observed,
            window=win,
            taper=taper,
            taper_ratio=taper_ratio,
            taper_type=taper_type,
        )
        synthetic = window_trace(
            trace=synthetic,
            window=win,
            taper=taper,
            taper_ratio=taper_ratio,
            taper_type=taper_type,
        )

        adjoint = fct(
            observed=observed,
            synthetic=synthetic,
            window=win,
            min_period=min_period,
            max_period=max_period,
            adjoint_src=adjoint_src,
            plot=plot,
            taper=taper,
            taper_ratio=taper_ratio,
            taper_type=taper_type,
        )

        if adjoint_src:
            adjoint["adjoint_source"] = window_trace(
                trace=adjoint["adjoint_source"],
                window=win,
                taper=taper,
                taper_ratio=taper_ratio,
                taper_type=taper_type,
            )
            if win == window[0]:
                full_ad_src = adjoint["adjoint_source"]
            else:
                full_ad_src.data += adjoint["adjoint_source"].data
            # individual_adj_srcs.append(adjoint["adjoint_source"])

        window_misfit.append((win[0], win[1], adjoint["misfit"]))
        trace_misfit += adjoint["misfit"]

    if plot:
        time = observed.times()
        generic_adjoint_source_plot(
            observed=observed.data,
            synthetic=synthetic.data,
            time=time,
            adjoint_source=adjoint["adjoint_source"],
            misfit=adjoint["misfit"],
            adjoint_source_name=adj_src_type,
        )
    if plot_filename:
        plt.savefig(plot_filename)
    else:
        plt.show()

    if "envelope_scaling" in kwargs and kwargs["envelope_scaling"]:
        full_ad_src.data *= env_weighting * norm_scaling_fac

    return AdjointSource(
        adj_src_type,
        misfit=trace_misfit,
        window_misfits=window_misfit,
        adjoint_source=full_ad_src,
        individual_ad_sources=individual_adj_srcs,
    )
Ejemplo n.º 28
0
    def plot(
        self, ax=None, plot_inner_boundary: bool = False,
    ):
        """
        Plots the domain
        Global domain is plotted using an equal area Mollweide projection.
        Smaller domains have eihter Orthographic projections or PlateCarree.

        :param ax: matplotlib axes, defaults to None
        :type ax: matplotlib.axes.Axes, optional
        :param plot_inner_boundary: plot the convex hull of the mesh
            surface nodes that lie inside the domain. Defaults to False
        :type plot_inner_boundary: bool, optional
        :return: The created GeoAxes instance.
        """
        import matplotlib.pyplot as plt

        transform = cp.crs.Geodetic()

        if plot_inner_boundary:
            raise LASIFError("Inner boundary is not plotted on simple domains")

        if self._is_global:
            projection = cp.crs.Mollweide()
            if ax is None:
                m = plt.axes(projection=projection)
            else:
                m = ax
            _plot_features(m, projection=projection)
            return m, projection

        lat_extent = self.max_lat - self.min_lat
        lon_extent = self.max_lon - self.min_lon
        max_extent = max(lat_extent, lon_extent)
        center_lat = np.mean((self.max_lat, self.min_lat))
        center_lon = np.mean((self.max_lon, self.min_lon))

        # Use a global plot for very large domains.
        if lat_extent >= 90.0 and lon_extent >= 90.0:
            projection = cp.crs.Mollweide()
            if ax is None:
                m = plt.axes(projection=projection)
            else:
                m = ax

        elif max_extent >= 75.0:
            projection = cp.crs.Orthographic(
                central_longitude=center_lon, central_latitude=center_lat,
            )
            if ax is None:
                m = plt.axes(projection=projection)
            else:
                m = ax
            m.set_extent(
                [
                    self.min_lon - 3.0,
                    self.max_lon + 3.0,
                    self.min_lat - 3.0,
                    self.max_lat + 3.0,
                ],
                crs=transform,
            )

        else:
            projection = cp.crs.PlateCarree(central_longitude=center_lon,)
            if ax is None:
                m = plt.axes(projection=projection,)
            else:
                m = ax

        boundary = self.get_sorted_corner_coords()

        _plot_lines(
            m,
            boundary,
            transform=cp.crs.PlateCarree(),
            color="red",
            lw=2,
            label="Domain Edge",
        )
        _plot_features(m, projection=projection)
        m.legend(framealpha=0.5, loc="lower right")

        return m, projection
Ejemplo n.º 29
0
def _sanity_checks(observed, synthetic):
    """
    Perform a number of basic sanity checks to assure the data is valid
    in a certain sense.

    It checks the types of both, the start time, sampling rate, number of
    samples, ...

    :param observed: The observed data.
    :type observed: :class:`obspy.core.trace.Trace`
    :param synthetic: The synthetic data.
    :type synthetic: :class:`obspy.core.trace.Trace`

    :raises: :class:`~lasif.LASIFError`
    """
    if not isinstance(observed, obspy.Trace):
        # Also accept Stream objects.
        if isinstance(observed, obspy.Stream) and len(observed) == 1:
            observed = observed[0]
        else:
            raise LASIFError(
                "Observed data must be an ObsPy Trace object., not {}"
                "".format(observed))
    if not isinstance(synthetic, obspy.Trace):
        if isinstance(synthetic, obspy.Stream) and len(synthetic) == 1:
            synthetic = synthetic[0]
        else:
            raise LASIFError("Synthetic data must be an ObsPy Trace object.")

    if observed.stats.npts != synthetic.stats.npts:
        raise LASIFError("Observed and synthetic data must have the "
                         "same number of samples.")

    sr1 = observed.stats.sampling_rate
    sr2 = synthetic.stats.sampling_rate

    if abs(sr1 - sr2) / sr1 >= 1e-5:
        raise LASIFError("Observed and synthetic data must have the "
                         "same sampling rate.")

    # Make sure data and synthetics start within half a sample interval.
    if (abs(observed.stats.starttime - synthetic.stats.starttime) >
            observed.stats.delta * 0.5):
        raise LASIFError("Observed and synthetic data must have the "
                         "same starttime.")

    ptp = sorted([observed.data.ptp(), synthetic.data.ptp()])
    if ptp[1] / ptp[0] >= 5:
        warnings.warn(
            "The amplitude difference between data and "
            "synthetic is fairly large.",
            LASIFWarning,
        )

    # Also check the components of the data to avoid silly mistakes of
    # users.
    if (len(
            set([
                observed.stats.channel[-1].upper(),
                synthetic.stats.channel[-1].upper(),
            ])) != 1):
        warnings.warn("The orientation code of synthetic and observed "
                      "data is not equal.")

    observed = observed.copy()
    synthetic = synthetic.copy()
    observed.data = np.require(observed.data,
                               dtype=np.float64,
                               requirements=["C"])
    synthetic.data = np.require(synthetic.data,
                                dtype=np.float64,
                                requirements=["C"])

    return observed, synthetic