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".' )
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)
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)
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
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
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, )
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, ) ]
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
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)
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, )
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))
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}")
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()
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()
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)
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
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'.")
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
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
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
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
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
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
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
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 )
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, )
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
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