Source code for gmprocess.waveform_processing.spectrum

"""Module for computation of theoretical amplitude spectrum methods."""

import numpy as np
from scipy.optimize import minimize
from obspy.geodetics.base import gps2dist_azimuth
from gmprocess.waveform_processing.processing_step import processing_step

OUTPUT_UNITS = ["ACC", "VEL", "DISP"]
M_TO_KM = 1.0 / 1000


[docs] @processing_step def fit_spectra( st, event, kappa=0.035, RP=0.55, VHC=0.7071068, FSE=2.0, density=2.8, shear_vel=3.7, R0=1.0, moment_factor=100, min_stress=0.1, max_stress=10000, config=None, ): """Fit spectra vaying stress_drop and moment. Args: st (gmprocess.core.stationstream.StationStream): Stream of data. event (gmprocess.utils.scalar_event.ScalarEvent): ScalarEvent object. kappa (float): Site diminution factor (sec). Typical value for active cruststal regions is about 0.03-0.04, and stable continental regions is about 0.006. RP (float): Partition of shear-wave energy into horizontal components. VHC (float): Partition of shear-wave energy into horizontal components 1 / np.sqrt(2.0). FSE (float): Free surface effect. density (float): Density at source (gm/cc). shear_vel (float): Shear-wave velocity at source (km/s). R0 (float): Reference distance (km). moment_factor (float): Multiplicative factor for setting bounds on moment, where the moment (from the catalog moment magnitude) is multiplied and divided by `moment_factor` to set the bounds for the spectral optimization. min_stress (float): Min stress for fit search (bars). max_stress (float): Max stress for fit search (bars). config (dict): Configuration dictionary (or None). See get_config(). Returns: StationStream with fitted spectra parameters. """ for tr in st: # Only do this for horizontal channels for which the smoothed spectra # has been computed. if tr.has_cached("smooth_signal_spectrum") and tr.has_parameter( "corner_frequencies" ): event_mag = event.magnitude event_lon = event.longitude event_lat = event.latitude dist = ( gps2dist_azimuth( lat1=event_lat, lon1=event_lon, lat2=tr.stats["coordinates"]["latitude"], lon2=tr.stats["coordinates"]["longitude"], )[0] * M_TO_KM ) # Use the smoothed spectra for fitting smooth_signal_dict = tr.get_cached("smooth_signal_spectrum") freq = np.array(smooth_signal_dict["freq"]) obs_spec = np.array(smooth_signal_dict["spec"]) # ----------------------------------------------------------------- # INITIAL VALUES # Need an approximate stress drop as initial guess stress_0 = np.sqrt(min_stress * max_stress) moment_0 = moment_from_magnitude(event_mag) # Array of initial values x0 = (np.log(moment_0), np.log(stress_0)) # Bounds stress_bounds = (np.log(min_stress), np.log(max_stress)) # multiplicative factor for moment bounds moment_bounds = ( x0[0] - np.log(moment_factor), x0[0] + np.log(moment_factor), ) bounds = (moment_bounds, stress_bounds) # Frequency limits for cost function freq_dict = tr.get_parameter("corner_frequencies") fmin = freq_dict["highpass"] fmax = freq_dict["lowpass"] # ----------------------------------------------------------------- # CONSTANT ARGUMENTS cargs = ( freq, obs_spec, fmin, fmax, dist, kappa, RP, VHC, FSE, shear_vel, density, R0, ) result = minimize( spectrum_cost, x0, args=cargs, method="L-BFGS-B", jac=False, bounds=bounds, tol=1e-4, options={"disp": False}, ) moment_fit = np.exp(result.x[0]) magnitude_fit = magnitude_from_moment(moment_fit) stress_drop_fit = np.exp(result.x[1]) f0_fit = brune_f0(moment_fit, stress_drop_fit) # Hessian (H) is in terms of normalized moment and stress drop # Covariance matrix is sigma^2 * H^-1. inv_hess = result.hess_inv.todense() # Estimate of sigma^2 is sum of squared residuals / (n - p) # NOTE: we are NOT accounting for the correlation across # frequencies and so we are underestimating the variance. SSR = result.fun sigma2 = SSR / (len(freq) - len(result.x)) COV = sigma2 * inv_hess sd = np.sqrt(np.diagonal(COV)) # mag_lower = magnitude_from_moment(np.exp(result.x[0]-sd[0])) # mag_upper = magnitude_from_moment(np.exp(result.x[0]+sd[0])) # stress_drop_lower = np.exp(result.x[1]-sd[1]) # stress_drop_upper = np.exp(result.x[1]+sd[1]) # Get the fitted spectrum and then calculate the goodness-of-fit # metrics fit_spec = model((moment_fit, stress_drop_fit), freq, dist, kappa) mean_squared_error = np.mean((obs_spec - fit_spec) ** 2) # R^2 (Coefficient of Determination) is defined as 1 minus the # residual sum of squares (SSR) divided by the total sum of squares # (SST) ssr = np.sum((obs_spec - fit_spec) ** 2) sst = np.sum((obs_spec - np.mean(obs_spec)) ** 2) r_squared = 1 - (ssr / sst) fit_spectra_dict = { "stress_drop": stress_drop_fit, "stress_drop_lnsd": sd[1], "epi_dist": dist, "kappa": kappa, "moment": moment_fit, "moment_lnsd": sd[0], "magnitude": magnitude_fit, "f0": f0_fit, "minimize_message": result.message, "minimize_success": result.success, "mean_squared_error": mean_squared_error, "R2": r_squared, } tr.set_parameter("fit_spectra", fit_spectra_dict) return st
def spectrum_cost( x, freq, obs_spec, fmin, fmax, dist, kappa, RP, VHC=0.7071068, FSE=2.0, shear_vel=3.7, density=2.8, R0=1.0, gs_mod="REA99", q_mod="REA99", crust_mod="BT15", ): """ Function to compute RMS log residuals for optimization. Args: x (tuple): Tuple of the moment (dyne-cm) and the stress drop (bars). freq (array): Numpy array of frequencies (Hz). obs_spec (array): Numpy array of observed Fourier spectral amplitudes. fmin (float): Minimum frequency to use in computing residuals. fmax (float): Maximum frequency to use in computing residuals. dist (float): Distance (km). kappa (float): Site diminution factor (sec). Typical value for active cruststal regions is about 0.03-0.04, and stable continental regions is about 0.006. RP (float): Partition of shear-wave energy into horizontal components. VHC (float): Partition of shear-wave energy into horizontal components 1 / np.sqrt(2.0). FSE (float): Free surface effect. shear_vel (float): Shear-wave velocity at source (km/s). density (float): Density at source (gm/cc). R0 (float): Reference distance (km). gs_model (str): Name of model for geometric attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) q_model (str): Name of model for anelastic attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) - 'none' for no anelastic attenuation crust_mod (str): Name of model for crustal amplification. Currently onlysupported value: - 'BT15' for Boore and Thompson (2015) - 'none' for no crustal amplification model. Returns: float: Sum of squared logarithmic residuals. """ # Exponentiate the paramters xexp = [] for xx in x: xexp.append(np.exp(xx)) mod_spec = model( xexp, freq, dist, kappa, RP, VHC, FSE, shear_vel, density, R0, gs_mod, q_mod, crust_mod, ) # Remove non-positive values of obs_spec and apply corner frequency # constraints keep = (obs_spec > 0) & (freq >= fmin) & (freq <= fmax) log_residuals = np.log(obs_spec[keep]) - np.log(mod_spec[keep]) return np.sum(log_residuals**2) def model( x, freq, dist, kappa, RP=0.55, VHC=0.7071068, FSE=2.0, shear_vel=3.7, density=2.8, R0=1.0, gs_mod="REA99", q_mod="REA99", crust_mod="BT15", ): """ Piece together a model of the ground motion spectrum. Args: x (tuple): Tuple of the natural log of moment (dyne-cm) and the natural log of stress drop (bars). freq (array): Numpy array of frequencies for computing spectra (Hz). dist (float): Distance (km). kappa (float): Site diminution factor (sec). Typical value for active cruststal regions is about 0.03-0.04, and stable continental regions is about 0.006. RP (float): Partition of shear-wave energy into horizontal components. VHC (float): Partition of shear-wave energy into horizontal components 1 / np.sqrt(2.0). FSE (float): Free surface effect. shear_vel (float): Shear-wave velocity at source (km/s). density (float): Density at source (gm/cc). R0 (float): Reference distance (km). gs_model (str): Name of model for geometric attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) q_model (str): Name of model for anelastic attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) - 'none' for no anelastic attenuation crust_mod (str): Name of model for crustal amplification. Currently only supported value: - 'BT15' for Boore and Thompson (2015) - 'none' for no crustal amplification model. Returns: Array of spectra model. """ source_mod = brune( freq, x[0], x[1], RP=RP, VHC=VHC, FSE=FSE, shear_vel=shear_vel, density=density, R0=R0, ) path_mod = path(freq, dist, gs_mod, q_mod) site_mod = site(freq, kappa, crust_mod) return source_mod * path_mod * site_mod def brune( freq, moment, stress_drop=150, RP=0.55, VHC=0.7071068, FSE=2.0, shear_vel=3.7, density=2.8, R0=1.0, output_units="ACC", ): """ Compute Brune (1970, 1971) earthquake source spectrum. Args: freq (array): Numpy array of frequencies for computing spectra (Hz). moment (float): Earthquake moment (dyne-cm). stress_drop (float): Earthquake stress drop (bars). RP (float): Partition of shear-wave energy into horizontal components. VHC (float): Partition of shear-wave energy into horizontal components 1 / np.sqrt(2.0). FSE (float): Free surface effect. shear_vel (float): Shear-wave velocity at source (km/s). density (float): Density at source (gm/cc). R0 (float): Reference distance (km). output_units (str): Time domain equivalent units for the output spectrum. One of: - "ACC" for acceleration, giving Fourier spectra units of cm/s. - "VEL" for velocity, giving Fourier spectra units of cm. - "DISP" Returns: Array of source spectra. """ if output_units not in OUTPUT_UNITS: raise ValueError("Unsupported value for output_units.") f0 = brune_f0(moment, stress_drop, shear_vel) S = 1 / (1 + (freq / f0) ** 2) C = RP * VHC * FSE / (4 * np.pi * density * shear_vel**3 * R0) * 1e-20 if output_units == "ACC": fpow = 2.0 elif output_units == "VEL": fpow = 1.0 elif output_units == "DISP": fpow = 0.0 displacement = C * moment * S return (2 * np.pi * freq) ** fpow * displacement def brune_f0(moment, stress_drop, shear_vel=3.7): """ Compute Brune's corner frequency. Args: moment (float): Earthquake moment (dyne-cm). stress_drop (float): Earthquake stress drop (bars). shear_vel (float): Shear-wave velocity at source (km/s). Returns: float: Brune corner frequency (Hz). """ f0 = 4.906e6 * shear_vel * (stress_drop / moment) ** (1.0 / 3.0) return f0 def brune_stress(moment, f0, shear_vel=3.7): """ Compute Brune's stress drop. Args: moment (float): Earthquake moment (dyne-cm). f0 (float): Brune corner frequency (Hz). shear_vel (float): Shear-wave velocity at source (km/s). Returns: float: Brune stress drop (bars). """ stress_drop = ((f0 / (4.906e6 * shear_vel)) ** 3) * moment return stress_drop def moment_from_magnitude(magnitude): """ Compute moment from moment magnitude. Args: magnitude (float): Moment magnitude. Returns: float: Seismic moment (dyne-cm). """ return 10 ** (1.5 * magnitude + 16.05) def magnitude_from_moment(moment): """ Compute moment from moment magnitude. Args: moment (float): Seismic moment (dyne-cm). Returns: float: Moment magnitude. """ return 2.0 / 3.0 * (np.log10(moment) - 16.05) def path(freq, dist, gs_mod="REA99", q_mod="REA99"): """ Path term, including geometric and anelastic attenuation. Args: freq (array): Numpy array of frequencies for computing spectra (Hz). dist (float): Distance (km). gs_model (str): Name of model for geometric attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) q_model (str): Name of model for anelastic attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) - 'none' for no anelastic attenuation Returns: Array of path effects. """ geom_spread = geometrical_spreading(freq, dist, model=gs_mod) ae_att = anelastic_attenuation(freq, dist, model=q_mod) return geom_spread * ae_att def site(freq, kappa, crust_mod="BT15"): """ Site term, including crustal amplification and kappa. Args: freq (array): Numpy array of frequencies for computing spectra (Hz). kappa (float): Site diminution factor (sec). Typical value for active cruststal regions is about 0.03-0.04, and stable continental regions is about 0.006. crust_mod (str): Name of model for crustal amplification. Currently only supported value: - 'BT15' for Boore and Thompson (2015) - 'none' for no crustal amplification model. """ crust_amp = crustal_amplification(freq, model=crust_mod) dim = np.exp(-np.pi * kappa * freq) return crust_amp * dim def crustal_amplification(freq, model="BT15"): """ Crustal amplification model. Args: freq (array): Numpy array of frequencies for computing spectra (Hz). model (str): Name of model for crustal amplification. Currently only supported value: - 'BT15' for Boore and Thompson (2015) - 'none' for no crustal amplification model. """ if model == "BT15": freq_tab = np.array( [ 0.001, 0.009, 0.025, 0.049, 0.081, 0.15, 0.37, 0.68, 1.11, 2.36, 5.25, 60.3, ] ) amplification_tab = np.array( [1.00, 1.01, 1.03, 1.06, 1.10, 1.19, 1.39, 1.58, 1.77, 2.24, 2.75, 4.49] ) # Interpolation should be linear freq, log amplification log_amp_tab = np.log(amplification_tab) log_amp_interp = np.interp(freq, freq_tab, log_amp_tab) crustal_amps = np.exp(log_amp_interp) elif model == "none": crustal_amps = np.ones_like(freq) else: raise ValueError("Unsupported crustal amplification model.") return crustal_amps def geometrical_spreading(freq, dist, model="REA99"): """ Effect of geometrical spreading. Args: freq (array): Numpy array of frequencies for computing spectra (Hz). dist (float): Distance (km). model (str): Name of model for geometric attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) Returns: geom (float): anelastic attenuation factor. """ if model == "REA99": dist_cross = 40.0 if dist <= dist_cross: geom = dist ** (-1.0) else: geom = (dist * dist_cross) ** (-0.5) else: raise ValueError("Unsupported anelastic attenuation model.") return geom def anelastic_attenuation(freq, dist, model="REA99"): """ Effect of anelastic attenuation. Args: freq (array): Numpy array of frequencies for computing spectra (Hz). dist (float): Distance (km). model (str): Name of model for anelastic attenuation. Currently only supported value: - 'REA99' for Raoof et al. (1999) - 'none' for no anelastic attenuation Returns: Array of aneastic attenuation factor. """ if model == "REA99": # Frequency dependent quality factor quality_factor = 180 * freq**0.45 cq = 3.5 anelastic = np.exp(-np.pi * freq * dist / quality_factor / cq) elif model == "none": anelastic = np.ones_like(freq) else: raise ValueError("Unsupported anelastic attenuation model.") return anelastic def finite_fault_factor(magnitude, model="BT15"): """ Finite fault factor for converting Rrup to an equivalent point source distance. Args: magnitude (float): Earthquake moment magnitude. model (str): Which model to use; currently only suppport "BT15". Returns: float: Adjusted distance. """ if model == "BT15": Mt1 = 5.744 Mt2 = 7.744 if magnitude < Mt1: c0 = 0.7497 c1 = 0.4300 c2 = 0.0 Mt = Mt1 elif magnitude < Mt2: c0 = 0.7497 c1 = 0.4300 c2 = -0.04875 Mt = Mt1 else: c0 = 1.4147 c1 = 0.2350 c2 = 0 Mt = Mt2 logH = c0 + c1 * (magnitude - Mt) + c2 * (magnitude - Mt) ** 2 h = 10 ** (logH) else: raise ValueError("Unsupported finite fault adjustment model.") return h