From 76a02732346325df1daedd720ba9798d6a4662e6 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 15:09:40 +0100 Subject: [PATCH 001/103] Removed commented out cells --- .../rutford_icequake_example.ipynb | 69 ++++--------------- 1 file changed, 14 insertions(+), 55 deletions(-) diff --git a/examples/rutford_icequake_example/rutford_icequake_example.ipynb b/examples/rutford_icequake_example/rutford_icequake_example.ipynb index de923ab..0954ac8 100644 --- a/examples/rutford_icequake_example/rutford_icequake_example.ipynb +++ b/examples/rutford_icequake_example/rutford_icequake_example.ipynb @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -59,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -77,17 +77,17 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Min. inter-station distance: 0.0193294780285 km\n", - "Therefore, optimal sensitive higher freq.: 98.2954633955 Hz\n", - "Max. inter-station distance: 0.0923463197348 km\n", - "Therefore, optimal sensitive lower freq.: 20.5747235565 Hz\n" + "Min. inter-station distance: 0.019329478028457248 km\n", + "Therefore, optimal sensitive higher freq.: 98.29546339548236 Hz\n", + "Max. inter-station distance: 0.09234631973482979 km\n", + "Therefore, optimal sensitive lower freq.: 20.574723556453616 Hz\n" ] } ], @@ -119,39 +119,6 @@ " " ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define a suitable event for using in this example:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "# # Specify data to load:\n", - "# archive = \"inputs/mseed_data_archive\"\n", - "# year = 2020\n", - "# julday = 1\n", - "# hour = 1\n", - "# station_codes_to_use = \"A\"\n", - "# freqmin = 10\n", - "# freqmax = 150\n", - "# mseed_dir = os.path.join(archive, str(year), str(julday).zfill(3))\n", - "\n", - "# # Load data:\n", - "# st = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), \"_\", str(hour).zfill(2), \"*\", station_codes_to_use, \"*\"))))\n", - "\n", - "# # And filter:\n", - "# st.filter('bandpass', freqmin=freqmin, freqmax=freqmax)\n", - "\n", - "# # And trim:\n", - "# st.trim(starttime=starttime, endtime=endtime)\n" - ] - }, { "cell_type": "code", "execution_count": null, @@ -168,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -1296,9 +1263,7 @@ { "cell_type": "code", "execution_count": 18, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -2342,9 +2307,7 @@ { "cell_type": "code", "execution_count": 21, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -4360,9 +4323,7 @@ { "cell_type": "code", "execution_count": 24, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -5375,9 +5336,7 @@ { "cell_type": "code", "execution_count": 25, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -10349,7 +10308,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -10363,7 +10322,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.9.16" } }, "nbformat": 4, From 2e493d4ab2cfe31165dcc6d9c0ed3a4da80c6646 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 15:12:55 +0100 Subject: [PATCH 002/103] Fixed bug with np.int being deprecated. Replaces with np.int64 --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index f88564c..34a56f5 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -85,10 +85,10 @@ def _fast_freq_domain_array_proc(data, max_sl, fs, target_freqs, xx, yy, n_stati # Construct data structure: nfft = (2.0**np.ceil(np.log2(n_t_samp))) nfft = np.array(nfft, dtype=np.int64) - Pxx_all = np.zeros((np.int((nfft/2)+1), n_stations), dtype=np.complex128) # Power spectra + Pxx_all = np.zeros((np.int64((nfft/2)+1), n_stations), dtype=np.complex128) # Power spectra dt = 1. / fs df = 1.0/(2.0*nfft*dt) - xf = np.linspace(0.0, 1.0/(2.0*dt), np.int((nfft/2)+1)) + xf = np.linspace(0.0, 1.0/(2.0*dt), np.int64((nfft/2)+1)) # Calculate power spectra for all stations: for sta_idx in range(n_stations): # Calculate spectra for current station: From f2685dd65dc565ca9963d9bd2b8367ee0fd6ac62 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 15:31:19 +0100 Subject: [PATCH 003/103] Started to strip out df.append()'s as this is deprecated and NOT RECOMMENDED FOR DATAFRAMES, replacing with lists which will be turned into a dataframe only when needed as per pandas /stackoverflow reccomendsations --- SeisSeeker/processing/detection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 34a56f5..71ee564 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -203,6 +203,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t 'pow1': [t_series_df_Z['power'][curr_peak_Z_idx]], 'pow2': [t_series_df_hor['power'][curr_peak_hor_idx]], 'slow1': [t_series_df_Z['slowness'][curr_peak_Z_idx]], 'slow2': [t_series_df_hor['slowness'][curr_peak_hor_idx]], 'bazi1': [t_series_df_Z['back_azi'][curr_peak_Z_idx]], 'bazi2': [t_series_df_hor['back_azi'][curr_peak_hor_idx]]}) + print(curr_event_df) events_df = events_df.append(curr_event_df) # And tidy: del t_Z_secs_after_start, t_hor_secs_after_start, Z_hor_phase_pair_idxs From e090150961346ddb6804fef649fb710190c74a74 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 15:51:13 +0100 Subject: [PATCH 004/103] removes tmp_dfs and removed creaton of unnessecary DataFrame 'sum_pows'. it is only used to add a column which can be done more effeciently (line wise and computationally) by df['sum_pows'] = sum_pows --- SeisSeeker/processing/detection.py | 46 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 71ee564..f68161a 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -218,33 +218,32 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # Calculate max power of P and S for each potential event: # events_overall_powers = events_df['pow1'].values + events_df['pow2'].values # Define datastores: - filt_events_df = pd.DataFrame() + filt_events_lst = [] # And loop over events, selecting only max. power events: tmp_count = 0 for index, row in events_df.iterrows(): tmp_count+=1 if tmp_count == 1: - tmp_df = pd.DataFrame() - tmp_df = tmp_df.append(row) + tmp_lst = [] + tmp_lst = tmp_lst.append(row) else: # Append event if phase within minimum event separation: - if obspy.UTCDateTime(row['t1']) - obspy.UTCDateTime(tmp_df['t1'].iloc[0]) < min_event_sep_s: + if obspy.UTCDateTime(row['t1']) - obspy.UTCDateTime(tmp_lst[0].t1) < min_event_sep_s: # Append event to compare: - tmp_df = tmp_df.append(row) + tmp_lst = tmp_lst.append(row) else: # Find best event from previous events: - combined_pows_tmp = tmp_df['pow1'].values + tmp_df['pow2'].values - max_power_idx = np.argmax(combined_pows_tmp) - filt_events_df = filt_events_df.append(tmp_df.iloc[max_power_idx]) + max_power_event = _find_max_power_event(tmp_lst) + filt_events_lst.append(max_power_event) # And start acrewing new events: - tmp_df = pd.DataFrame() - tmp_df = tmp_df.append(row) + tmp_lst = [] + tmp_lst = tmp_lst.append(row) # And calculate highest power event for final window: - combined_pows_tmp = tmp_df['pow1'].values + tmp_df['pow2'].values - max_power_idx = np.argmax(combined_pows_tmp) - filt_events_df = filt_events_df.append(tmp_df.iloc[max_power_idx]) - + max_power_event = _find_max_power_event(tmp_lst) + filt_events_lst.append(max_power_event) + # Now make new DataFrame + filt_event_df = pd.DataFrame(filt_events_df) # And sort indices: filt_events_df.reset_index(drop=True, inplace=True) @@ -252,8 +251,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # (using same max. power method) # Append summed powers, for sorting: sum_pows = filt_events_df['pow1'].values + filt_events_df['pow2'].values - sum_pows_df = pd.DataFrame({'sum_pows': sum_pows}) - filt_events_df = filt_events_df.join(sum_pows_df) + filt_events_df['sum_pows'] = sum_pows # Remove t2 duplicates, keep highest summed power: filt_events_df = filt_events_df.sort_values('sum_pows').drop_duplicates(subset='t2', keep='last') # And remove sum_pows column: @@ -266,6 +264,22 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t return events_df +def _find_max_power_event(events): + """ + Find the maximum power event from a list of events + + Parameters: + ---------- + events : list + list (of Dataframe Rows) of event + + """ + pow1_tmp = np.array([event.pow1 for event in events]) + pow2_tmp = np.array([event.pow2 for event in events]) + combined_pows_tmp = pow1_tmp + pow2_tmp + max_power_idx = np.argmax(combined_pows_tmp) + max_power_event = events[max_power_idx] + return max_power_event def _submit_parallel_fast_freq_domain_array_proc(procnum, return_dict_Pfreq_all, data_curr_run, max_sl, fs, target_freqs, xx, yy, n_stations, n_t_samp, remove_autocorr): """Function to submit parallel runs of _fast_freq_domain_array_proc() function.""" From 750b77dccf9c86e629951e97b807e37b7403aceb Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 15:54:43 +0100 Subject: [PATCH 005/103] Switched to using pandas internal inplace=True kwarg to do operations on filt_events_df rather than overwritign it with a view of itself --- SeisSeeker/processing/detection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index f68161a..ae28079 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -253,9 +253,10 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t sum_pows = filt_events_df['pow1'].values + filt_events_df['pow2'].values filt_events_df['sum_pows'] = sum_pows # Remove t2 duplicates, keep highest summed power: - filt_events_df = filt_events_df.sort_values('sum_pows').drop_duplicates(subset='t2', keep='last') + filt_events_df.sort_values('sum_pows', inplace=True) + filt_events_df.drop_duplicates(subset='t2', keep='last', inplace=True) # And remove sum_pows column: - filt_events_df = filt_events_df.drop(columns=['sum_pows']) + filt_events_df.drop(columns=['sum_pows'], inplace=True) # And output df: events_df = filt_events_df.copy() From 0d7a1669e04c783173b69f1fac3e8d7ca4147042 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 16:13:18 +0100 Subject: [PATCH 006/103] Refactored out putting of time series to again remove DataFrame appending --- SeisSeeker/processing/detection.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index ae28079..4c5991c 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -791,7 +791,7 @@ def run_array_proc(self): continue # Create datastore: - out_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) + datastore = {'t': [], 'power': [], 'slowness': [], 'back_azi': []} # Load data: st = self._load_day_of_data(year, julday, hour=hour) @@ -846,8 +846,11 @@ def run_array_proc(self): else: for t_serie in t_series: t_series_out.append( str(starttime_this_st + t_serie) ) - tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) - out_df = out_df.append(tmp_df) + datastore['t'].append(t_series_out) + datastore['power'].append(powers) + datastore['slowness'].append(slownesses) + datastore['back_azi'].append(back_azis) + out_df = pd.DataFrame(datastore, columns=['t', 'power', 'slowness', 'back_azi']) out_df.reset_index(drop=True, inplace=True) # And save data out: From 3eb40afdde0d2caf759d401dc06f36a750b5b49d Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 16:34:15 +0100 Subject: [PATCH 007/103] Reveerse previous refactoring to enough outpuf df is written properly, for some reason making a dict first itsn working here? --- SeisSeeker/processing/detection.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 4c5991c..48b70a7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -791,7 +791,7 @@ def run_array_proc(self): continue # Create datastore: - datastore = {'t': [], 'power': [], 'slowness': [], 'back_azi': []} + store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) # Load data: st = self._load_day_of_data(year, julday, hour=hour) @@ -846,11 +846,8 @@ def run_array_proc(self): else: for t_serie in t_series: t_series_out.append( str(starttime_this_st + t_serie) ) - datastore['t'].append(t_series_out) - datastore['power'].append(powers) - datastore['slowness'].append(slownesses) - datastore['back_azi'].append(back_azis) - out_df = pd.DataFrame(datastore, columns=['t', 'power', 'slowness', 'back_azi']) + tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) + out_df = pd.concat([store_df, tmp_df]) out_df.reset_index(drop=True, inplace=True) # And save data out: From cbece34fccaddbf9deb6682c8e4f56b4cf402610 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 16:40:43 +0100 Subject: [PATCH 008/103] Replaced DataFrame appending with pd.contact(list_of_dfs_to_append)in _phase_associator --- SeisSeeker/processing/detection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 48b70a7..d95515e 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -173,7 +173,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t Function to perform phase association for numba implementation. """ # Setup events datastores: - events_df = pd.DataFrame() + list_of_curr_event_dfs = [] # Find back-azimuths associated with phase picks: bazis_Z = t_series_df_Z['back_azi'].values[peaks_Z] bazis_hor = t_series_df_hor['back_azi'].values[peaks_hor] @@ -203,8 +203,10 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t 'pow1': [t_series_df_Z['power'][curr_peak_Z_idx]], 'pow2': [t_series_df_hor['power'][curr_peak_hor_idx]], 'slow1': [t_series_df_Z['slowness'][curr_peak_Z_idx]], 'slow2': [t_series_df_hor['slowness'][curr_peak_hor_idx]], 'bazi1': [t_series_df_Z['back_azi'][curr_peak_Z_idx]], 'bazi2': [t_series_df_hor['back_azi'][curr_peak_hor_idx]]}) + list_of_curr_event_dfs.append(curr_event_df) print(curr_event_df) - events_df = events_df.append(curr_event_df) + + events_df = pd.concat(list_of_curr_event_dfs) # And tidy: del t_Z_secs_after_start, t_hor_secs_after_start, Z_hor_phase_pair_idxs gc.collect() From a719678de37cf9b7ffb30db463d0d6e4a8d0a673 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 16:45:56 +0100 Subject: [PATCH 009/103] Fixed bug with tmp_lst's .append() returns None for a string --- SeisSeeker/processing/detection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index d95515e..946f739 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -227,12 +227,13 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t tmp_count+=1 if tmp_count == 1: tmp_lst = [] - tmp_lst = tmp_lst.append(row) + tmp_lst.append(row) else: + # Append event if phase within minimum event separation: if obspy.UTCDateTime(row['t1']) - obspy.UTCDateTime(tmp_lst[0].t1) < min_event_sep_s: # Append event to compare: - tmp_lst = tmp_lst.append(row) + tmp_lst.append(row) else: # Find best event from previous events: max_power_event = _find_max_power_event(tmp_lst) @@ -240,7 +241,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # And start acrewing new events: tmp_lst = [] - tmp_lst = tmp_lst.append(row) + tmp_lst.append(row) # And calculate highest power event for final window: max_power_event = _find_max_power_event(tmp_lst) filt_events_lst.append(max_power_event) From cf02bb433d9334fee5b462566536579f3595c303 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 16:57:00 +0100 Subject: [PATCH 010/103] replaced append with concat --- SeisSeeker/processing/detection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 946f739..e73ef0b 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -246,7 +246,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t max_power_event = _find_max_power_event(tmp_lst) filt_events_lst.append(max_power_event) # Now make new DataFrame - filt_event_df = pd.DataFrame(filt_events_df) + filt_events_df = pd.DataFrame(filt_events_lst) # And sort indices: filt_events_df.reset_index(drop=True, inplace=True) @@ -1101,7 +1101,7 @@ def detect_events(self, verbosity=0): """ print("Note: not yet implemented.") # Create datastore: - events_df_all = pd.DataFrame() + evenbts_df_all = pd.DataFrame() # Loop over array proc outdir data: for fname in glob.glob(os.path.join(self.outdir, "detection_t_series_*_chZ.csv")): f_uid = fname[-20:-8] @@ -1178,7 +1178,7 @@ def detect_events(self, verbosity=0): events_df = self._calc_uncertainties(events_df, t_series_df_Z, t_series_df_hor, verbosity=verbosity) # Append to datastore: - events_df_all = events_df_all.append(events_df) + events_df_all = pd.concat([events_df_all, events_df]) # Plot detected, phase-associated picks: if verbosity > 1: From bb2c315edb82c3078b4d36a93b7d891f9a770884 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 4 Aug 2023 17:08:16 +0100 Subject: [PATCH 011/103] typo fix --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index e73ef0b..aad5b78 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1101,7 +1101,7 @@ def detect_events(self, verbosity=0): """ print("Note: not yet implemented.") # Create datastore: - evenbts_df_all = pd.DataFrame() + events_df_all = pd.DataFrame() # Loop over array proc outdir data: for fname in glob.glob(os.path.join(self.outdir, "detection_t_series_*_chZ.csv")): f_uid = fname[-20:-8] From 56d23f4ec885b708ae3354f9c19cff6ea356f824 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 7 Aug 2023 14:59:25 +0100 Subject: [PATCH 012/103] moved run_array_proc to appear before under-the hood functins to iprove readability --- SeisSeeker/processing/detection.py | 219 ++++++++++++++--------------- 1 file changed, 108 insertions(+), 111 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index aad5b78..701bfd2 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -547,6 +547,114 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo # And load existing detection instance, if specified: if preload_fname: self.load(preload_fname) + + def run_array_proc(self): + """Function to run core array processing. + Performed in frequency domain. Involves applying phase (equiv. to time) shift + for each frequency, over a range of specified slownesses. + Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" + + # Loop over years: + for year in range(self.starttime.year, self.endtime.year+1): + # Loop over days: + for julday in range(1,367): + # Do some filtering for first and last years: + if year == self.starttime.year: + if julday < self.starttime.julday: + continue # Ignore day, as out of range + if year == self.endtime.year: + if julday > self.endtime.julday: + continue # Ignore day, as out of range + + # And process data: + + # Loop over channels: + for self.channel_curr in self.channels_to_use: + print("="*60) + print("Processing data for year "+str(year)+", day "+str(julday).zfill(3)+", channel "+self.channel_curr) + + # And process for individual hours: + # (to reduce memory usage) + for hour in range(24): + print("Processing for hour", str(hour).zfill(2)) + if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: + continue + if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): + continue + + # Create datastore: + store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) + + # Load data: + st = self._load_day_of_data(year, julday, hour=hour) + # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) + try: + starttime_this_st = st[0].stats.starttime + except IndexError: + # And skip if no data: + print("Skipping hour as no data") + del st + gc.collect() + continue + + # And loop over minutes (to save on memory issues): + for minute in range(60): + # Check whether specified window is greater than a minute in duration: + if self.endtime - self.starttime > 60: + # Check time within specified run window: + if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: + continue + if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): + continue + elif self.starttime.minute != minute: + continue + + # Trim data: + st_trimmed = st.copy() + if self.win_len_s > self.win_step_inc_s: + self.win_pad_s = self.win_len_s + else: + self.win_pad_s = 0. + if self.endtime - self.starttime > 60: + st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), + endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) + else: + st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) + + # Run array processing: + # (to get power in slowness space) + Psum_all = self._beamforming(st_trimmed) + del st_trimmed + gc.collect() + + # Calculate time-series outputs (for detection) from data: + t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) + + # And append to data out: + t_series_out = [] + if self.endtime - self.starttime > 60: + for t_serie in t_series: + t_series_out.append( str(starttime_this_st + (minute*60) + t_serie) ) + else: + for t_serie in t_series: + t_series_out.append( str(starttime_this_st + t_serie) ) + tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) + out_df = pd.concat([store_df, tmp_df]) + out_df.reset_index(drop=True, inplace=True) + + # And save data out: + out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", + str(starttime_this_st.hour).zfill(2), "00", "_ch", self.channel_curr[-1], ".csv"))) + out_df.to_csv(out_fname, index=False) + + # And append fname to history: + self.out_fnames_array_proc.append(out_fname) + + # And clear memory: + del Psum_all, t_series, powers, slownesses, back_azis + gc.collect() + + return None def _setup_array_receiver_coords(self): @@ -756,117 +864,6 @@ def _beamforming(self, st_trimmed, verbosity=0): return Psum_all - - def run_array_proc(self): - """Function to run core array processing. - Performed in frequency domain. Involves applying phase (equiv. to time) shift - for each frequency, over a range of specified slownesses. - Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" - # Prep. stations df: - #self._setup_array_receiver_coords() - - # Loop over years: - for year in range(self.starttime.year, self.endtime.year+1): - # Loop over days: - for julday in range(1,367): - # Do some filtering for first and last years: - if year == self.starttime.year: - if julday < self.starttime.julday: - continue # Ignore day, as out of range - if year == self.endtime.year: - if julday > self.endtime.julday: - continue # Ignore day, as out of range - - # And process data: - - # Loop over channels: - for self.channel_curr in self.channels_to_use: - print("="*60) - print("Processing data for year "+str(year)+", day "+str(julday).zfill(3)+", channel "+self.channel_curr) - - # And process for individual hours: - # (to reduce memory usage) - for hour in range(24): - print("Processing for hour", str(hour).zfill(2)) - if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: - continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): - continue - - # Create datastore: - store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) - - # Load data: - st = self._load_day_of_data(year, julday, hour=hour) - # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) - try: - starttime_this_st = st[0].stats.starttime - except IndexError: - # And skip if no data: - print("Skipping hour as no data") - del st - gc.collect() - continue - - # And loop over minutes (to save on memory issues): - for minute in range(60): - # Check whether specified window is greater than a minute in duration: - if self.endtime - self.starttime > 60: - # Check time within specified run window: - if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: - continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): - continue - elif self.starttime.minute != minute: - continue - - # Trim data: - st_trimmed = st.copy() - if self.win_len_s > self.win_step_inc_s: - self.win_pad_s = self.win_len_s - else: - self.win_pad_s = 0. - if self.endtime - self.starttime > 60: - st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), - endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) - else: - st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) - - # Run array processing: - # (to get power in slowness space) - Psum_all = self._beamforming(st_trimmed) - del st_trimmed - gc.collect() - - # Calculate time-series outputs (for detection) from data: - t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) - - # And append to data out: - t_series_out = [] - if self.endtime - self.starttime > 60: - for t_serie in t_series: - t_series_out.append( str(starttime_this_st + (minute*60) + t_serie) ) - else: - for t_serie in t_series: - t_series_out.append( str(starttime_this_st + t_serie) ) - tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) - out_df = pd.concat([store_df, tmp_df]) - out_df.reset_index(drop=True, inplace=True) - - # And save data out: - out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", - str(starttime_this_st.hour).zfill(2), "00", "_ch", self.channel_curr[-1], ".csv"))) - out_df.to_csv(out_fname, index=False) - - # And append fname to history: - self.out_fnames_array_proc.append(out_fname) - - # And clear memory: - del Psum_all, t_series, powers, slownesses, back_azis - gc.collect() - - return None - def _calculate_mad(self, x, scale=1.4826): """ Calculates the Median Absolute Deviation (MAD) of the input array x. From 87eeaedadd3cdec930d8255aa5638f70ec3ca88c Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 7 Aug 2023 15:05:19 +0100 Subject: [PATCH 013/103] moved run_array_proc function to improve readability --- SeisSeeker/processing/detection.py | 220 ++++++++++++++--------------- 1 file changed, 109 insertions(+), 111 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index aad5b78..2f5b5ec 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -547,7 +547,116 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo # And load existing detection instance, if specified: if preload_fname: self.load(preload_fname) + + def run_array_proc(self): + """Function to run core array processing. + Performed in frequency domain. Involves applying phase (equiv. to time) shift + for each frequency, over a range of specified slownesses. + Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" + # Prep. stations df: + #self._setup_array_receiver_coords() + + # Loop over years: + for year in range(self.starttime.year, self.endtime.year+1): + # Loop over days: + for julday in range(1,367): + # Do some filtering for first and last years: + if year == self.starttime.year: + if julday < self.starttime.julday: + continue # Ignore day, as out of range + if year == self.endtime.year: + if julday > self.endtime.julday: + continue # Ignore day, as out of range + + # And process data: + + # Loop over channels: + for self.channel_curr in self.channels_to_use: + print("="*60) + print("Processing data for year "+str(year)+", day "+str(julday).zfill(3)+", channel "+self.channel_curr) + + # And process for individual hours: + # (to reduce memory usage) + for hour in range(24): + print("Processing for hour", str(hour).zfill(2)) + if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: + continue + if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): + continue + + # Create datastore: + store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) + + # Load data: + st = self._load_day_of_data(year, julday, hour=hour) + # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) + try: + starttime_this_st = st[0].stats.starttime + except IndexError: + # And skip if no data: + print("Skipping hour as no data") + del st + gc.collect() + continue + + # And loop over minutes (to save on memory issues): + for minute in range(60): + # Check whether specified window is greater than a minute in duration: + if self.endtime - self.starttime > 60: + # Check time within specified run window: + if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: + continue + if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): + continue + elif self.starttime.minute != minute: + continue + + # Trim data: + st_trimmed = st.copy() + if self.win_len_s > self.win_step_inc_s: + self.win_pad_s = self.win_len_s + else: + self.win_pad_s = 0. + if self.endtime - self.starttime > 60: + st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), + endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) + else: + st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) + # Run array processing: + # (to get power in slowness space) + Psum_all = self._beamforming(st_trimmed) + del st_trimmed + gc.collect() + + # Calculate time-series outputs (for detection) from data: + t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) + + # And append to data out: + t_series_out = [] + if self.endtime - self.starttime > 60: + for t_serie in t_series: + t_series_out.append( str(starttime_this_st + (minute*60) + t_serie) ) + else: + for t_serie in t_series: + t_series_out.append( str(starttime_this_st + t_serie) ) + tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) + out_df = pd.concat([store_df, tmp_df]) + out_df.reset_index(drop=True, inplace=True) + + # And save data out: + out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", + str(starttime_this_st.hour).zfill(2), "00", "_ch", self.channel_curr[-1], ".csv"))) + out_df.to_csv(out_fname, index=False) + + # And append fname to history: + self.out_fnames_array_proc.append(out_fname) + + # And clear memory: + del Psum_all, t_series, powers, slownesses, back_azis + gc.collect() + + return None def _setup_array_receiver_coords(self): """Function to setup station receiver coords in correct format for @@ -756,117 +865,6 @@ def _beamforming(self, st_trimmed, verbosity=0): return Psum_all - - def run_array_proc(self): - """Function to run core array processing. - Performed in frequency domain. Involves applying phase (equiv. to time) shift - for each frequency, over a range of specified slownesses. - Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" - # Prep. stations df: - #self._setup_array_receiver_coords() - - # Loop over years: - for year in range(self.starttime.year, self.endtime.year+1): - # Loop over days: - for julday in range(1,367): - # Do some filtering for first and last years: - if year == self.starttime.year: - if julday < self.starttime.julday: - continue # Ignore day, as out of range - if year == self.endtime.year: - if julday > self.endtime.julday: - continue # Ignore day, as out of range - - # And process data: - - # Loop over channels: - for self.channel_curr in self.channels_to_use: - print("="*60) - print("Processing data for year "+str(year)+", day "+str(julday).zfill(3)+", channel "+self.channel_curr) - - # And process for individual hours: - # (to reduce memory usage) - for hour in range(24): - print("Processing for hour", str(hour).zfill(2)) - if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: - continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): - continue - - # Create datastore: - store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) - - # Load data: - st = self._load_day_of_data(year, julday, hour=hour) - # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) - try: - starttime_this_st = st[0].stats.starttime - except IndexError: - # And skip if no data: - print("Skipping hour as no data") - del st - gc.collect() - continue - - # And loop over minutes (to save on memory issues): - for minute in range(60): - # Check whether specified window is greater than a minute in duration: - if self.endtime - self.starttime > 60: - # Check time within specified run window: - if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: - continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): - continue - elif self.starttime.minute != minute: - continue - - # Trim data: - st_trimmed = st.copy() - if self.win_len_s > self.win_step_inc_s: - self.win_pad_s = self.win_len_s - else: - self.win_pad_s = 0. - if self.endtime - self.starttime > 60: - st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), - endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) - else: - st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) - - # Run array processing: - # (to get power in slowness space) - Psum_all = self._beamforming(st_trimmed) - del st_trimmed - gc.collect() - - # Calculate time-series outputs (for detection) from data: - t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) - - # And append to data out: - t_series_out = [] - if self.endtime - self.starttime > 60: - for t_serie in t_series: - t_series_out.append( str(starttime_this_st + (minute*60) + t_serie) ) - else: - for t_serie in t_series: - t_series_out.append( str(starttime_this_st + t_serie) ) - tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) - out_df = pd.concat([store_df, tmp_df]) - out_df.reset_index(drop=True, inplace=True) - - # And save data out: - out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", - str(starttime_this_st.hour).zfill(2), "00", "_ch", self.channel_curr[-1], ".csv"))) - out_df.to_csv(out_fname, index=False) - - # And append fname to history: - self.out_fnames_array_proc.append(out_fname) - - # And clear memory: - del Psum_all, t_series, powers, slownesses, back_azis - gc.collect() - - return None - def _calculate_mad(self, x, scale=1.4826): """ Calculates the Median Absolute Deviation (MAD) of the input array x. From b86ba2a6f20daab3151d1c754591c2346a39b344 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 7 Aug 2023 16:42:53 +0100 Subject: [PATCH 014/103] reorgansied / redefined assumed unput filenames may want to re-do dir structure at some point too --- SeisSeeker/processing/detection.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 2f5b5ec..0bb8961 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -724,15 +724,19 @@ def _load_day_of_data(self, year, julday, hour=None): """Function to load a day of data.""" # Load in data: mseed_dir = os.path.join(self.archivedir, str(year), str(julday).zfill(3)) + print(mseed_dir) st = obspy.Stream() for index, row in self.stations_df.iterrows(): station = row['Name'] for channel in self.channels_to_use: + fname = f'{station}_{year}????T{hour}*.{channel}' + full_fname = os.path.join(mseed_dir, fname) + print() try: if hour: - st_tmp = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), "_", str(hour).zfill(2), "*", station, "*", channel, "*")))) + st_tmp = obspy.read(full_fname) else: - st_tmp = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), "_*", station, "*", channel, "*")))) + st_tmp = obspy.read(full_fname) for tr in st_tmp: st.append(tr) except: From dbe8d692e5643f2e398d087d81a97aab2c5a8df0 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 8 Aug 2023 09:56:58 +0100 Subject: [PATCH 015/103] removed print statement for trialling change of fnames --- SeisSeeker/processing/detection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 0bb8961..6ffbd29 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -579,7 +579,7 @@ def run_array_proc(self): # (to reduce memory usage) for hour in range(24): print("Processing for hour", str(hour).zfill(2)) - if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: + if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: continue if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): continue @@ -731,7 +731,6 @@ def _load_day_of_data(self, year, julday, hour=None): for channel in self.channels_to_use: fname = f'{station}_{year}????T{hour}*.{channel}' full_fname = os.path.join(mseed_dir, fname) - print() try: if hour: st_tmp = obspy.read(full_fname) From d1b584ea260c5e5c959f7a2e98887614004ac096 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 4 Dec 2023 17:19:51 +0000 Subject: [PATCH 016/103] Changed outputting of detection_t_series so every minute is written out Longerterm fix would be to write these as hour time series most probably --- SeisSeeker/processing/detection.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 6ffbd29..150ebfe 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -620,9 +620,10 @@ def run_array_proc(self): if self.endtime - self.starttime > 60: st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) + print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)) + print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) else: st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) - # Run array processing: # (to get power in slowness space) Psum_all = self._beamforming(st_trimmed) @@ -646,7 +647,8 @@ def run_array_proc(self): # And save data out: out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", - str(starttime_this_st.hour).zfill(2), "00", "_ch", self.channel_curr[-1], ".csv"))) + str(starttime_this_st.hour).zfill(2), str(starttime_this_st.minute).zfill(2), + "_ch", self.channel_curr[-1], ".csv"))) out_df.to_csv(out_fname, index=False) # And append fname to history: @@ -729,17 +731,19 @@ def _load_day_of_data(self, year, julday, hour=None): for index, row in self.stations_df.iterrows(): station = row['Name'] for channel in self.channels_to_use: - fname = f'{station}_{year}????T{hour}*.{channel}' - full_fname = os.path.join(mseed_dir, fname) + # fname = f'{station}_{year}????T{hour}*.{channel}' + # fname = f'{}' + # full_fname = os.path.join(mseed_dir, fname) try: if hour: - st_tmp = obspy.read(full_fname) + st_tmp = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), "_", str(hour).zfill(2), "*", station, "*", channel, "*")))) else: - st_tmp = obspy.read(full_fname) + st_tmp = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), "_*", station, "*", channel, "*")))) for tr in st_tmp: st.append(tr) except: print("No data for "+station+", channel = "+channel+". Skipping this data.") + # print(full_fname) continue # Merge data: st.detrend('demean') @@ -758,6 +762,7 @@ def _load_day_of_data(self, year, julday, hour=None): def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" + print(st) self.n_win = int(((st[0].stats.endtime - self.win_pad_s) - st[0].stats.starttime) / self.win_step_inc_s) # (Note: endtime - self.win_pad_s as pass extra padding via trimmed st) self.fs = st[0].stats.sampling_rate self.n_t_samp = int(self.win_len_s * self.fs) # num samples in time From cdec62fe8b8f37e753518beedc860e944d03cbf3 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 4 Dec 2023 17:20:31 +0000 Subject: [PATCH 017/103] removed print(st) used to debug trimming issues --- SeisSeeker/processing/detection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 150ebfe..68002bb 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -762,7 +762,6 @@ def _load_day_of_data(self, year, julday, hour=None): def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" - print(st) self.n_win = int(((st[0].stats.endtime - self.win_pad_s) - st[0].stats.starttime) / self.win_step_inc_s) # (Note: endtime - self.win_pad_s as pass extra padding via trimmed st) self.fs = st[0].stats.sampling_rate self.n_t_samp = int(self.win_len_s * self.fs) # num samples in time From 71d47b1e2e46788906b8b9f2c26eb48609828c69 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 5 Dec 2023 14:09:10 +0000 Subject: [PATCH 018/103] patched bug where date ranges were being used that resulted in empty streams this then triggers self.n_win to evalute to -20 which raises a negative dimension error. this is occuring because Stream objects are being trimmed incorrectly at the END of a query testing for starttime >= query and endtime <= query patches this more testign needed to ensure this is working as intended --- SeisSeeker/processing/detection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 68002bb..cff3b6b 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -604,9 +604,9 @@ def run_array_proc(self): # Check whether specified window is greater than a minute in duration: if self.endtime - self.starttime > 60: # Check time within specified run window: - if self.starttime > obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: + if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): + if self.endtime <= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): continue elif self.starttime.minute != minute: continue @@ -636,6 +636,7 @@ def run_array_proc(self): # And append to data out: t_series_out = [] if self.endtime - self.starttime > 60: + ### DEBUG ME! for t_serie in t_series: t_series_out.append( str(starttime_this_st + (minute*60) + t_serie) ) else: From 451b4614d91b6e1bc0c568be7b9c8431efdac01d Mon Sep 17 00:00:00 2001 From: Joseph Asplet <32487558+Jasplet@users.noreply.github.com> Date: Wed, 6 Dec 2023 10:33:53 +0000 Subject: [PATCH 019/103] Update README.md Added comment on why this fork exists --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 02e1bde..8f37424 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,5 @@ SeisSeeker is a package for performing beamforming for earthquake detection. It A decription of how the package works can be found here: Thomas S. Hudson, Alex M. Brisbourne, Sofia-Katerina Kufner, J-Michael Kendall, and Andy M. Smith. (in review). "Array processing in cryoseismology". Submitted to: The Cryosphere. + +This version of SeisSeeker has been modified for application to monitoring of North Sea Microseismicity From 268838cdac1b274d35906a3edbdd3167ddc3ee74 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 6 Dec 2023 10:46:31 +0000 Subject: [PATCH 020/103] Fixed bug allocating correct times to beamformed time series Using starttime_this_st results in the INCORRECT times being in some cases If the loaded data is trimmed so that it does not start on the hour. Then starttime_this_st is not at the hour either. This breaks for queries longer than 1 minute. My fix is instead to assign time from the minute trimmed chunks of data. For queries longer than a minute this means the minute bemform time series are given the correct times. For queries < 60 s the trimmed data starts at query start, so times should be correctly assigned from that reference point. --- SeisSeeker/processing/detection.py | 36 ++++++++++++++---------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index cff3b6b..fcabd7a 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -624,6 +624,7 @@ def run_array_proc(self): print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) else: st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) + time_this_minute_st = st_trimmed[0].stats.starttime # Run array processing: # (to get power in slowness space) Psum_all = self._beamforming(st_trimmed) @@ -635,29 +636,26 @@ def run_array_proc(self): # And append to data out: t_series_out = [] - if self.endtime - self.starttime > 60: - ### DEBUG ME! - for t_serie in t_series: - t_series_out.append( str(starttime_this_st + (minute*60) + t_serie) ) - else: - for t_serie in t_series: - t_series_out.append( str(starttime_this_st + t_serie) ) + for t_serie in t_series: + t_series_out.append( str(time_this_minute_st + t_serie) ) + tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) - out_df = pd.concat([store_df, tmp_df]) - out_df.reset_index(drop=True, inplace=True) + store_df = pd.concat([store_df, tmp_df]) + + store_df.reset_index(drop=True, inplace=True) - # And save data out: - out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", - str(starttime_this_st.hour).zfill(2), str(starttime_this_st.minute).zfill(2), - "_ch", self.channel_curr[-1], ".csv"))) - out_df.to_csv(out_fname, index=False) + # And save data out: + out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", + str(starttime_this_st.hour).zfill(2), str(starttime_this_st.minute).zfill(2), + "_ch", self.channel_curr[-1], ".csv"))) + store_df.to_csv(out_fname, index=False) - # And append fname to history: - self.out_fnames_array_proc.append(out_fname) + # And append fname to history: + self.out_fnames_array_proc.append(out_fname) - # And clear memory: - del Psum_all, t_series, powers, slownesses, back_azis - gc.collect() + # And clear memory: + del Psum_all, t_series, powers, slownesses, back_azis + gc.collect() return None From a8fbca777a34118d88992c5f53cf15e41d1c5422 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 6 Dec 2023 15:48:09 +0000 Subject: [PATCH 021/103] started to re-work use of Julian days to mm-dd --- SeisSeeker/processing/detection.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index e82676a..19af135 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -718,23 +718,21 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): print("="*60) - def _load_day_of_data(self, year, julday, hour=None): + def _load_day_of_data(self, year, month, day, hour=None): """Function to load a day of data.""" # Load in data: - mseed_dir = os.path.join(self.archivedir, str(year), str(julday).zfill(3)) + mseed_dir = os.path.join(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) print(mseed_dir) st = obspy.Stream() for index, row in self.stations_df.iterrows(): station = row['Name'] for channel in self.channels_to_use: - # fname = f'{station}_{year}????T{hour}*.{channel}' - # fname = f'{}' - # full_fname = os.path.join(mseed_dir, fname) + if hour: + timestamp = f'{year:02d}{month:02d}{day:02d}T{hour:02d}0000' + else: + timestamp = f'{year:02d}{month:02d}{day:02d}T*' try: - if hour: - st_tmp = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), "_", str(hour).zfill(2), "*", station, "*", channel, "*")))) - else: - st_tmp = obspy.read(os.path.join(mseed_dir, ''.join((str(year), str(julday).zfill(3), "_*", station, "*", channel, "*")))) + st_tmp = obspy.read(f'{mseed_dir}/{timestamp}_{station}_{channel}.mseed') for tr in st_tmp: st.append(tr) except: From ed05d6b2a6cbe3520e5308bda9bbe775578ac31f Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 6 Dec 2023 15:53:18 +0000 Subject: [PATCH 022/103] adding datetime as dependency to better wrangle with dates --- SeisSeeker/processing/detection.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 19af135..1b82ee5 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -16,6 +16,7 @@ from mpl_toolkits.mplot3d import Axes3D import os, sys import obspy +import datetime from scipy.signal import find_peaks from numba import jit, objmode, prange, set_num_threads import gc @@ -387,7 +388,7 @@ class setup_detection: ---------- archivedir : str Path to data archive. Data archive must be of specific format: - /YEAR/JULDAY/YEARJULDAY_*STATION_COMP.* + /YEAR/MONTH/DAY/YYYYMMDDTHHMMSS_*STATION_COMP.* outdir : str Path to directory to save outputs to. @@ -481,7 +482,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo ---------- archivedir : str Path to data archive. Data archive must be of specific format: - /YEAR/JULDAY/YEARJULDAY_*STATION_COMP.* + /YEAR/MONTH/DAY/YYYYMMDDTHHMMSS_*STATION_COMP.* outdir : str Path to directory to save outputs to. @@ -902,7 +903,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # (done like this to avoid unnneccessary read ins, improving eff.) event_phase_arr_time = obspy.UTCDateTime(row['t1']) if count == 0: - st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.julday, hour=event_phase_arr_time.hour) + st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.month, event_phase_arr_time.day, hour=event_phase_arr_time.hour) # Find uncertainties: # ------- For vertical -------: @@ -924,7 +925,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_phase_arr_time = obspy.UTCDateTime(row['t1']) # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: - st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.julday, hour=event_phase_arr_time.hour) + st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.month, event_phase_arr_time.day, hour=event_phase_arr_time.hour) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1006,7 +1007,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_phase_arr_time = obspy.UTCDateTime(row['t2']) # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: - st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.julday, hour=event_phase_arr_time.hour) + st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.month, event_phase_arr_time.day, hour=event_phase_arr_time.hour) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1378,7 +1379,7 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo If True, returns st and composite_st. Optional. Default = False. """ # Load in raw mseed data: - st = self._load_day_of_data(arrival_time.year, arrival_time.julday, hour=arrival_time.hour) + st = self._load_day_of_data(arrival_time.year, arrival_time.month, arrival_time.day, hour=arrival_time.hour) # And trim data: st.trim(starttime=arrival_time-t_before_s, endtime=arrival_time+t_after_s) From 3a7f293862b29383a6943ea75ea54ede16b4bdbb Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 6 Dec 2023 16:04:02 +0000 Subject: [PATCH 023/103] switched to iterating over datetime objects this reduces the number of nested for loops which can only be a good thing :-) --- SeisSeeker/processing/detection.py | 183 ++++++++++++++--------------- 1 file changed, 89 insertions(+), 94 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 1b82ee5..00b71dd 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -555,107 +555,102 @@ def run_array_proc(self): for each frequency, over a range of specified slownesses. Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" - # Loop over years: - for year in range(self.starttime.year, self.endtime.year+1): - # Loop over days: - for julday in range(1,367): - # Do some filtering for first and last years: - if year == self.starttime.year: - if julday < self.starttime.julday: - continue # Ignore day, as out of range - if year == self.endtime.year: - if julday > self.endtime.julday: - continue # Ignore day, as out of range - - # And process data: - - # Loop over channels: - for self.channel_curr in self.channels_to_use: - print("="*60) - print("Processing data for year "+str(year)+", day "+str(julday).zfill(3)+", channel "+self.channel_curr) - - # And process for individual hours: - # (to reduce memory usage) - for hour in range(24): - print("Processing for hour", str(hour).zfill(2)) - if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: - continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): + # Find number of days to run array processing over + dt_start = self.starttime.datetime + dt_end = self.endtime.datetime + ndays = (dt_end - dt_start).days + 1 + query_dates = [dt_start + d for d in range(0,ndays)] + for date in query_dates: + # Loop over dates within start/end range: + year = date.year + month = date.month + day = date.day + # Loop over channels: + for self.channel_curr in self.channels_to_use: + print("="*60) + print(f"Processing data for day {date}, channel {self.channel_curr}") + # And process for individual hours: + # (to reduce memory usage) + for hour in range(24): + print("Processing for hour", str(hour).zfill(2)) + if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: + continue + if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): + continue + + # Create datastore: + store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) + + # Load data: + st = self._load_day_of_data(year, julday, hour=hour) + # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) + try: + starttime_this_st = st[0].stats.starttime + except IndexError: + # And skip if no data: + print("Skipping hour as no data") + del st + gc.collect() + continue + + # And loop over minutes (to save on memory issues): + for minute in range(60): + # Check whether specified window is greater than a minute in duration: + if self.endtime - self.starttime > 60: + # Check time within specified run window: + if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: + continue + if self.endtime <= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): + continue + elif self.starttime.minute != minute: continue + + # Trim data: + st_trimmed = st.copy() + if self.win_len_s > self.win_step_inc_s: + self.win_pad_s = self.win_len_s + else: + self.win_pad_s = 0. + if self.endtime - self.starttime > 60: + st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), + endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) + print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)) + print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) + else: + st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) + time_this_minute_st = st_trimmed[0].stats.starttime + # Run array processing: + # (to get power in slowness space) + Psum_all = self._beamforming(st_trimmed) + del st_trimmed + gc.collect() - # Create datastore: - store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) - - # Load data: - st = self._load_day_of_data(year, julday, hour=hour) - # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) - try: - starttime_this_st = st[0].stats.starttime - except IndexError: - # And skip if no data: - print("Skipping hour as no data") - del st - gc.collect() - continue + # Calculate time-series outputs (for detection) from data: + t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) - # And loop over minutes (to save on memory issues): - for minute in range(60): - # Check whether specified window is greater than a minute in duration: - if self.endtime - self.starttime > 60: - # Check time within specified run window: - if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: - continue - if self.endtime <= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): - continue - elif self.starttime.minute != minute: - continue - - # Trim data: - st_trimmed = st.copy() - if self.win_len_s > self.win_step_inc_s: - self.win_pad_s = self.win_len_s - else: - self.win_pad_s = 0. - if self.endtime - self.starttime > 60: - st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), - endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) - print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)) - print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) - else: - st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) - time_this_minute_st = st_trimmed[0].stats.starttime - # Run array processing: - # (to get power in slowness space) - Psum_all = self._beamforming(st_trimmed) - del st_trimmed - gc.collect() - - # Calculate time-series outputs (for detection) from data: - t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) - - # And append to data out: - t_series_out = [] - for t_serie in t_series: - t_series_out.append( str(time_this_minute_st + t_serie) ) + # And append to data out: + t_series_out = [] + for t_serie in t_series: + t_series_out.append( str(time_this_minute_st + t_serie) ) - tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) - store_df = pd.concat([store_df, tmp_df]) - - store_df.reset_index(drop=True, inplace=True) + tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) + store_df = pd.concat([store_df, tmp_df]) - # And save data out: - out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", - str(starttime_this_st.hour).zfill(2), str(starttime_this_st.minute).zfill(2), - "_ch", self.channel_curr[-1], ".csv"))) - store_df.to_csv(out_fname, index=False) + store_df.reset_index(drop=True, inplace=True) - # And append fname to history: - self.out_fnames_array_proc.append(out_fname) + # And save data out: + out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", + str(starttime_this_st.hour).zfill(2), str(starttime_this_st.minute).zfill(2), + "_ch", self.channel_curr[-1], ".csv"))) + store_df.to_csv(out_fname, index=False) - # And clear memory: - del Psum_all, t_series, powers, slownesses, back_azis - gc.collect() - + # And append fname to history: + self.out_fnames_array_proc.append(out_fname) + + # And clear memory: + del Psum_all, t_series, powers, slownesses, back_azis + gc.collect() + return None def _setup_array_receiver_coords(self): From 32817c4d9ef81769cfa7122b80656ecbbfb8a135 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 6 Dec 2023 16:14:15 +0000 Subject: [PATCH 024/103] changed from datetime to date objects finished jday -> mmdd retool --- SeisSeeker/processing/detection.py | 34 +++++++++++++----------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 00b71dd..f734bc8 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -556,15 +556,12 @@ def run_array_proc(self): Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" # Find number of days to run array processing over - dt_start = self.starttime.datetime - dt_end = self.endtime.datetime + dt_start = self.starttime.date + dt_end = self.endtime.date ndays = (dt_end - dt_start).days + 1 query_dates = [dt_start + d for d in range(0,ndays)] for date in query_dates: # Loop over dates within start/end range: - year = date.year - month = date.month - day = date.day # Loop over channels: for self.channel_curr in self.channels_to_use: print("="*60) @@ -572,18 +569,18 @@ def run_array_proc(self): # And process for individual hours: # (to reduce memory usage) for hour in range(24): - print("Processing for hour", str(hour).zfill(2)) - if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour) + 3600: + # Loop over every hour in every day.. + print(f"Processing for hour: {hour:02d}") + if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue - if self.endtime < obspy.UTCDateTime(year=year, julday=julday, hour=hour): + if self.endtime < obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): continue # Create datastore: store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) # Load data: - st = self._load_day_of_data(year, julday, hour=hour) - # starttime_this_day = obspy.UTCDateTime(year=year, julday=julday) + st = self._load_day_of_data(year=date.year, month=date.month, day=date.day, hour=hour) try: starttime_this_st = st[0].stats.starttime except IndexError: @@ -598,9 +595,9 @@ def run_array_proc(self): # Check whether specified window is greater than a minute in duration: if self.endtime - self.starttime > 60: # Check time within specified run window: - if self.starttime >= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute) + 60: + if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute) + 60: continue - if self.endtime <= obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute): + if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute): continue elif self.starttime.minute != minute: continue @@ -612,10 +609,10 @@ def run_array_proc(self): else: self.win_pad_s = 0. if self.endtime - self.starttime > 60: - st_trimmed.trim(starttime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute), - endtime=obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) - print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)) - print(obspy.UTCDateTime(year=year, julday=julday, hour=hour, minute=minute)+60+self.win_pad_s) + st_trimmed.trim(starttime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute), + endtime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)+60+self.win_pad_s) + print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)) + print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, minute=minute)+60+self.win_pad_s) else: st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) time_this_minute_st = st_trimmed[0].stats.starttime @@ -639,9 +636,8 @@ def run_array_proc(self): store_df.reset_index(drop=True, inplace=True) # And save data out: - out_fname = os.path.join(self.outdir, ''.join(("detection_t_series_", str(year).zfill(4), str(julday).zfill(3), "_", - str(starttime_this_st.hour).zfill(2), str(starttime_this_st.minute).zfill(2), - "_ch", self.channel_curr[-1], ".csv"))) + outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{starttime_this_st.hour:02d}00_ch{self.channel_curr[-1].csv}' + out_fname = os.path.join(self.outdir, outfile) store_df.to_csv(out_fname, index=False) # And append fname to history: From 001b832142fd542b7ba0135fefc5a70ac798dd17 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 7 Dec 2023 09:00:50 +0000 Subject: [PATCH 025/103] bug fix in assigning query dates and fix bug in printing of times, i wasnt adding the right hour! --- SeisSeeker/processing/detection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index f734bc8..8a4c22f 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -559,7 +559,7 @@ def run_array_proc(self): dt_start = self.starttime.date dt_end = self.endtime.date ndays = (dt_end - dt_start).days + 1 - query_dates = [dt_start + d for d in range(0,ndays)] + query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] for date in query_dates: # Loop over dates within start/end range: # Loop over channels: @@ -612,7 +612,7 @@ def run_array_proc(self): st_trimmed.trim(starttime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute), endtime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)+60+self.win_pad_s) print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)) - print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, minute=minute)+60+self.win_pad_s) + print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)+60+self.win_pad_s) else: st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) time_this_minute_st = st_trimmed[0].stats.starttime @@ -636,7 +636,7 @@ def run_array_proc(self): store_df.reset_index(drop=True, inplace=True) # And save data out: - outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{starttime_this_st.hour:02d}00_ch{self.channel_curr[-1].csv}' + outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{starttime_this_st.hour:02d}00_ch{self.channel_curr[-1]}.csv' out_fname = os.path.join(self.outdir, outfile) store_df.to_csv(out_fname, index=False) @@ -1095,7 +1095,7 @@ def detect_events(self, verbosity=0): events_df_all = pd.DataFrame() # Loop over array proc outdir data: for fname in glob.glob(os.path.join(self.outdir, "detection_t_series_*_chZ.csv")): - f_uid = fname[-20:-8] + f_uid = fname[-21:-8] # Check if in list to process: if fname in self.out_fnames_array_proc: # And load in data: From 45036d446d57aa961971d09ed5de1c1bcab6e8e0 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 7 Dec 2023 17:01:43 +0000 Subject: [PATCH 026/103] debugged issue rollowing over at the end of a day. streams where starttime == endtime were possible, and now shouldn't be. also moved deletion of power spectra and garbage collection to be within the minute for loop as there doesn't appear to be a reason why we wouldnt do this --- SeisSeeker/processing/detection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 8a4c22f..c5cfc79 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -573,7 +573,7 @@ def run_array_proc(self): print(f"Processing for hour: {hour:02d}") if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue - if self.endtime < obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): + if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): continue # Create datastore: @@ -633,6 +633,10 @@ def run_array_proc(self): tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) store_df = pd.concat([store_df, tmp_df]) + # And clear memory: + del Psum_all, t_series, powers, slownesses, back_azis + gc.collect() + store_df.reset_index(drop=True, inplace=True) # And save data out: @@ -643,9 +647,7 @@ def run_array_proc(self): # And append fname to history: self.out_fnames_array_proc.append(out_fname) - # And clear memory: - del Psum_all, t_series, powers, slownesses, back_azis - gc.collect() + return None From 2af88220ec9fabe020a450355fc5b215cde924b8 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 7 Dec 2023 18:41:46 +0000 Subject: [PATCH 027/103] comment out pykonal as it is broken? --- SeisSeeker/processing/lookup_table_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/lookup_table_manager.py b/SeisSeeker/processing/lookup_table_manager.py index f283f11..34b3747 100755 --- a/SeisSeeker/processing/lookup_table_manager.py +++ b/SeisSeeker/processing/lookup_table_manager.py @@ -19,7 +19,7 @@ import skfmm # For fast-marching travel-time lookup tables # import ttcrpy.rgrid as ttcrpy_rgrid # For ray-tracing based incidence angles import gc -import pykonal # For ray-tracing based incidence angles +#import pykonal # For ray-tracing based incidence angles #----------------------------------------------- Define constants and parameters ----------------------------------------------- From dc847156938d0a18a3566b8f7367175708bad28a Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 19 Feb 2024 13:01:08 +0000 Subject: [PATCH 028/103] made fnames an argument to detect_events. this allows us the specify which detection t_series we want to run the detecter on --- SeisSeeker/processing/detection.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index c5cfc79..5908234 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -981,6 +981,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.colorbar(im) plt.grid() plt.show() + fig.savefig('slow_spac_vert.png', dpi=600) + # ------- For horizontal -------: # And find FWHM for t2 pick: @@ -1066,7 +1068,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.colorbar(im) plt.grid() plt.show() - + fig.savefig('slow_spac_horz.png', dpi=600) # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], 'slow2_err': [slow2_err], 'bazi1_err': [bazi1_err], 'bazi2_err': [bazi2_err]}) @@ -1083,7 +1085,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi return events_df - def detect_events(self, verbosity=0): + def detect_events(self, verbosity=0, fnames=None): """Function to detect events, based on the power time-series generated by run_array_proc(). Note: Currently, only Median Absolute Deviation triggering is implemented. @@ -1096,7 +1098,9 @@ def detect_events(self, verbosity=0): # Create datastore: events_df_all = pd.DataFrame() # Loop over array proc outdir data: - for fname in glob.glob(os.path.join(self.outdir, "detection_t_series_*_chZ.csv")): + if fnames is None: + fnames = glob.glob(os.path.join(self.outdir, "detection_t_series_*_chZ.csv")) + for fname in fnames: f_uid = fname[-21:-8] # Check if in list to process: if fname in self.out_fnames_array_proc: @@ -1179,7 +1183,7 @@ def detect_events(self, verbosity=0): print("Event phase associations:") print(events_df) print("="*40) - fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(6,4)) + fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(9,6)) # Plot power: ax[0].plot(t_series_df_Z['t'], t_series_df_Z['power'], label="Vertical power") ax[0].plot(t_series_df_hor['t'], t_series_df_hor['power'], label="Horizontal power") @@ -1202,7 +1206,9 @@ def detect_events(self, verbosity=0): # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) for i in range(3): ax[i].xaxis.set_major_locator(plt.MaxNLocator(3)) + fig.savefig('Phase_assocaition.png', dpi=600) plt.show() + return events_df_all From f2f27e44a5fc10b5e432e58b7f28ea459e904218 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 10:30:05 +0000 Subject: [PATCH 029/103] output splowness spaces for all events (i.e., the wont overwrtie) --- SeisSeeker/processing/detection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 5908234..652a6b7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -968,7 +968,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # ------- End vertical ------- # Plot slowness space that used for uncertainty, if specified: - if verbosity > 1: + if verbosity >= 1: fig = plt.figure() Axes3D(fig) rad = np.linspace(0, self.max_sl, Psum_opt.shape[0]) @@ -981,7 +981,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.colorbar(im) plt.grid() plt.show() - fig.savefig('slow_spac_vert.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/{self.starttime}_slow_spac_vert.png', dpi=600) # ------- For horizontal -------: @@ -1055,7 +1055,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # ------- End horizontal ------- # Plot slowness space that used for uncertainty, if specified: - if verbosity > 1: + if verbosity >= 1: fig = plt.figure() Axes3D(fig) rad = np.linspace(0, self.max_sl, Psum_opt.shape[0]) @@ -1068,7 +1068,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.colorbar(im) plt.grid() plt.show() - fig.savefig('slow_spac_horz.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/{self.starttime}_slow_spac_vert.png', dpi=600) # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], 'slow2_err': [slow2_err], 'bazi1_err': [bazi1_err], 'bazi2_err': [bazi2_err]}) From 8ebc399d113a4cb9c7063a8db6d25201c62a52a5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 10:53:38 +0000 Subject: [PATCH 030/103] naming of output plots (slowness surfaces) now matches detected event times --- SeisSeeker/processing/detection.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 652a6b7..563cb12 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -980,9 +980,10 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') plt.colorbar(im) plt.grid() - plt.show() - fig.savefig(f'{self.outdir}/plots/{self.starttime}_slow_spac_vert.png', dpi=600) - + event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' + event_time_stamp = f'{event_phase_arr_time.hour:02s}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' + fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + plt.close() # ------- For horizontal -------: # And find FWHM for t2 pick: @@ -1067,8 +1068,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') plt.colorbar(im) plt.grid() - plt.show() - fig.savefig(f'{self.outdir}/plots/{self.starttime}_slow_spac_vert.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + plt.close() # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], 'slow2_err': [slow2_err], 'bazi1_err': [bazi1_err], 'bazi2_err': [bazi2_err]}) From e0d55180a51d643c646fe94d33b042781fdd916d Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 11:03:56 +0000 Subject: [PATCH 031/103] comment out note on mad_window_length not being implemented --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 563cb12..d2c2c6a 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1095,7 +1095,7 @@ def detect_events(self, verbosity=0, fnames=None): - mad_multiplier - min_event_sep_s """ - print("Note: not yet implemented.") + # print("Note: not yet implemented.") # Create datastore: events_df_all = pd.DataFrame() # Loop over array proc outdir data: From 47c3ffb6ee2157b27b89b6fc9081b52a3edde1b7 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 15:48:17 +0000 Subject: [PATCH 032/103] fix typo bug in string formatting --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index d2c2c6a..05edf20 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -981,7 +981,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.colorbar(im) plt.grid() event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' - event_time_stamp = f'{event_phase_arr_time.hour:02s}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' + event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) plt.close() From 85a5f2a433ab2f9f131474be16bf78fbcc14f023 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 15:59:59 +0000 Subject: [PATCH 033/103] another typo --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 05edf20..daf317a 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1068,7 +1068,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') plt.colorbar(im) plt.grid() - fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_horz.png', dpi=600) plt.close() # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], From fd9c0346b135570a213161bd0848f5cd86b2a987 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 16:29:47 +0000 Subject: [PATCH 034/103] added plotting of detection traces --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index daf317a..5bc8031 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1207,7 +1207,7 @@ def detect_events(self, verbosity=0, fnames=None): # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) for i in range(3): ax[i].xaxis.set_major_locator(plt.MaxNLocator(3)) - fig.savefig('Phase_assocaition.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/Phase_assocaition_{f_uid}.png', dpi=600) plt.show() From e137372b2a51a40a6313c1a9ab4bf1abe9089269 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Feb 2024 16:30:49 +0000 Subject: [PATCH 035/103] added subdirs for different plots. users currently have to make these!! --- SeisSeeker/processing/detection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 5bc8031..e6246db 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -982,7 +982,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.grid() event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' - fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/vespagrams/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) plt.close() # ------- For horizontal -------: @@ -1068,7 +1068,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') plt.colorbar(im) plt.grid() - fig.savefig(f'{self.outdir}/plots/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_horz.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/vespagrams/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_horz.png', dpi=600) plt.close() # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], @@ -1207,7 +1207,7 @@ def detect_events(self, verbosity=0, fnames=None): # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) for i in range(3): ax[i].xaxis.set_major_locator(plt.MaxNLocator(3)) - fig.savefig(f'{self.outdir}/plots/Phase_assocaition_{f_uid}.png', dpi=600) + fig.savefig(f'{self.outdir}/plots/detection_t_series/Phase_assocaition_{f_uid}.png', dpi=600) plt.show() From 7a93213927a6fd77b9db4c0801430eaa5cef32e0 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 21 Feb 2024 10:27:50 +0000 Subject: [PATCH 036/103] date loop tweak and sttipped out one reliance on appending dataframes, which causes a crash if no events are picked in one section ! --- SeisSeeker/processing/detection.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index e6246db..efe7a22 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -197,17 +197,23 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # Organise outputs into useful form: if verbosity > 1: print("Writing events") + + curr_events = {'t1':[],'t2':[], 'power1': [], 'power2':[], 'slowness1':[], + 'slowness2':[], 'bazi1':[], 'bazi2':[]} + for event_idx in range(len(Z_hor_phase_pair_idxs)): curr_peak_Z_idx = Z_hor_phase_pair_idxs[event_idx][0] curr_peak_hor_idx = Z_hor_phase_pair_idxs[event_idx][1] - curr_event_df = pd.DataFrame({'t1': [t_series_df_Z['t'][curr_peak_Z_idx]], 't2': [t_series_df_hor['t'][curr_peak_hor_idx]], - 'pow1': [t_series_df_Z['power'][curr_peak_Z_idx]], 'pow2': [t_series_df_hor['power'][curr_peak_hor_idx]], - 'slow1': [t_series_df_Z['slowness'][curr_peak_Z_idx]], 'slow2': [t_series_df_hor['slowness'][curr_peak_hor_idx]], - 'bazi1': [t_series_df_Z['back_azi'][curr_peak_Z_idx]], 'bazi2': [t_series_df_hor['back_azi'][curr_peak_hor_idx]]}) - list_of_curr_event_dfs.append(curr_event_df) - print(curr_event_df) - - events_df = pd.concat(list_of_curr_event_dfs) + curr_events['t1'].append(t_series_df_Z['t'][curr_peak_Z_idx]) + curr_events['t2'].append(t_series_df_hor['t'][curr_peak_hor_idx]) + curr_events['power1'].append(t_series_df_Z['power'][curr_peak_Z_idx]) + curr_events['power2'].append(t_series_df_hor['power'][curr_peak_hor_idx]) + curr_events['slowness1'].append(t_series_df_Z['slowness'][curr_peak_Z_idx]) + curr_events['slowness2'].append(t_series_df_hor['slowness'][curr_peak_hor_idx]) + curr_events['bazi1'].append(t_series_df_Z['back_azi'][curr_peak_Z_idx]) + curr_events['bazi2'].append(t_series_df_hor['back_azi'][curr_peak_hor_idx]) + + events_df = pd.DataFrame(curr_events) # And tidy: del t_Z_secs_after_start, t_hor_secs_after_start, Z_hor_phase_pair_idxs gc.collect() @@ -560,6 +566,7 @@ def run_array_proc(self): dt_end = self.endtime.date ndays = (dt_end - dt_start).days + 1 query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] + print(query_dates) for date in query_dates: # Loop over dates within start/end range: # Loop over channels: @@ -611,8 +618,6 @@ def run_array_proc(self): if self.endtime - self.starttime > 60: st_trimmed.trim(starttime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute), endtime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)+60+self.win_pad_s) - print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)) - print(obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)+60+self.win_pad_s) else: st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) time_this_minute_st = st_trimmed[0].stats.starttime From d385cdb5f28874866993ee0e428019c5f04898ef Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 21 Feb 2024 10:34:56 +0000 Subject: [PATCH 037/103] dont need to add extra day. that makes dates loop over and extra day --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index efe7a22..2c900c5 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -564,7 +564,7 @@ def run_array_proc(self): # Find number of days to run array processing over dt_start = self.starttime.date dt_end = self.endtime.date - ndays = (dt_end - dt_start).days + 1 + ndays = (dt_end - dt_start).days query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] print(query_dates) for date in query_dates: From 4fca205a5d4bb76214cd55227e0df01659526dd5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 11 Apr 2024 15:45:32 +0100 Subject: [PATCH 038/103] fixed naming of beam power /bazi/slowness columns --- SeisSeeker/processing/detection.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 2c900c5..8b7db7c 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -206,10 +206,10 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t curr_peak_hor_idx = Z_hor_phase_pair_idxs[event_idx][1] curr_events['t1'].append(t_series_df_Z['t'][curr_peak_Z_idx]) curr_events['t2'].append(t_series_df_hor['t'][curr_peak_hor_idx]) - curr_events['power1'].append(t_series_df_Z['power'][curr_peak_Z_idx]) - curr_events['power2'].append(t_series_df_hor['power'][curr_peak_hor_idx]) - curr_events['slowness1'].append(t_series_df_Z['slowness'][curr_peak_Z_idx]) - curr_events['slowness2'].append(t_series_df_hor['slowness'][curr_peak_hor_idx]) + curr_events['pow1'].append(t_series_df_Z['power'][curr_peak_Z_idx]) + curr_events['pow2'].append(t_series_df_hor['power'][curr_peak_hor_idx]) + curr_events['slow1'].append(t_series_df_Z['slowness'][curr_peak_Z_idx]) + curr_events['slow2'].append(t_series_df_hor['slowness'][curr_peak_hor_idx]) curr_events['bazi1'].append(t_series_df_Z['back_azi'][curr_peak_Z_idx]) curr_events['bazi2'].append(t_series_df_hor['back_azi'][curr_peak_hor_idx]) @@ -564,7 +564,8 @@ def run_array_proc(self): # Find number of days to run array processing over dt_start = self.starttime.date dt_end = self.endtime.date - ndays = (dt_end - dt_start).days + ndays = (dt_end - dt_start).days + 1 + print(ndays) query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] print(query_dates) for date in query_dates: @@ -969,6 +970,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi Pxx_curr = 0 idx_diff = Psum_opt.shape[1] dbazi = 360 / Psum_opt.shape[1] + bazi1_err = idx_diff * dbazi # ------- End vertical ------- @@ -982,7 +984,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi ax = plt.subplot(projection="polar") ax.set_theta_offset(np.pi/2) ax.set_theta_direction(-1) - im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') + im = ax.pcolormesh(th, r, Psum_opt/Psum_opt.max(), cmap='inferno') plt.colorbar(im) plt.grid() event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' From 2df9a92e8c8b7b4f30047ba983e5a2deb1560d4e Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 15 Apr 2024 16:28:52 +0100 Subject: [PATCH 039/103] fixed naming of params in events df --- SeisSeeker/processing/detection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 8b7db7c..7c6f2cb 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -198,8 +198,8 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t if verbosity > 1: print("Writing events") - curr_events = {'t1':[],'t2':[], 'power1': [], 'power2':[], 'slowness1':[], - 'slowness2':[], 'bazi1':[], 'bazi2':[]} + curr_events = {'t1':[],'t2':[], 'pow1': [], 'pow2':[], 'slow1':[], + 'slow2':[], 'bazi1':[], 'bazi2':[]} for event_idx in range(len(Z_hor_phase_pair_idxs)): curr_peak_Z_idx = Z_hor_phase_pair_idxs[event_idx][0] @@ -736,7 +736,7 @@ def _load_day_of_data(self, year, month, day, hour=None): for tr in st_tmp: st.append(tr) except: - print("No data for "+station+", channel = "+channel+". Skipping this data.") + print(f"No data for {station}, channel = {channel}, timestamp {timestamp}. Skipping this data.") # print(full_fname) continue # Merge data: From c415e360479792ee4694316167152964e55f689d Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 24 Apr 2024 10:39:36 +0100 Subject: [PATCH 040/103] added .DS_store files to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 027ebd1..81310e5 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json # And bespoke ignores: ./**/*.old + +# Mac OS specific +*.DS_store From d62b58cc5149286164517557916d18be8aaa96e5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 24 Apr 2024 16:50:11 +0100 Subject: [PATCH 041/103] bugfix --- SeisSeeker/processing/detection.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 7c6f2cb..540ddf0 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -11,7 +11,7 @@ # Import neccessary modules: import pandas as pd import numpy as np -import matplotlib +from pathlib import Path, PurePath import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import os, sys @@ -721,10 +721,11 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): def _load_day_of_data(self, year, month, day, hour=None): """Function to load a day of data.""" # Load in data: - mseed_dir = os.path.join(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) - print(mseed_dir) + mseed_dir = PurePath.join(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) + # print(mseed_dir) st = obspy.Stream() for index, row in self.stations_df.iterrows(): + # [J Asplet - think about replacing station DataFrame with StatonXML object] station = row['Name'] for channel in self.channels_to_use: if hour: @@ -989,7 +990,9 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi plt.grid() event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' - fig.savefig(f'{self.outdir}/plots/vespagrams/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + vesp_figpath = Path(self.outdir, 'plots', ' vespagrams') + vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist + fig.savefig({vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) plt.close() # ------- For horizontal -------: @@ -1075,7 +1078,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') plt.colorbar(im) plt.grid() - fig.savefig(f'{self.outdir}/plots/vespagrams/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_horz.png', dpi=600) + fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_horz.png', dpi=600) plt.close() # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], @@ -1214,7 +1217,9 @@ def detect_events(self, verbosity=0, fnames=None): # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) for i in range(3): ax[i].xaxis.set_major_locator(plt.MaxNLocator(3)) - fig.savefig(f'{self.outdir}/plots/detection_t_series/Phase_assocaition_{f_uid}.png', dpi=600) + figpath = Path(self.outdir, 'plots', 'detection_t_series') + figpath.mkdir(parents=True, exist_ok=True) + fig.savefig(f'{figpath}/Phase_association_{f_uid}.png', dpi=600) plt.show() From ad9574d99848fe17c61cd1fa5351c5498cf21890 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 24 Apr 2024 16:50:47 +0100 Subject: [PATCH 042/103] bugfix #2 --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 540ddf0..234516a 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -992,7 +992,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' vesp_figpath = Path(self.outdir, 'plots', ' vespagrams') vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist - fig.savefig({vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + fig.savefig('{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) plt.close() # ------- For horizontal -------: From ce3129be5c59df06fbf124b83988c38ff594f3a1 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 24 Apr 2024 16:52:12 +0100 Subject: [PATCH 043/103] fix Path bug --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 234516a..25848ff 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -721,7 +721,7 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): def _load_day_of_data(self, year, month, day, hour=None): """Function to load a day of data.""" # Load in data: - mseed_dir = PurePath.join(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) + mseed_dir = Path(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) # print(mseed_dir) st = obspy.Stream() for index, row in self.stations_df.iterrows(): From 762751ddc8c575510c03520a129591dd21f53839 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 24 Apr 2024 17:28:53 +0100 Subject: [PATCH 044/103] re-added f-string --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 25848ff..af8255f 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -992,7 +992,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' vesp_figpath = Path(self.outdir, 'plots', ' vespagrams') vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist - fig.savefig('{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) + fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) plt.close() # ------- For horizontal -------: From 799e7f6b31ee4e6c1f03eb1e17e880a54f635032 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 26 Apr 2024 16:33:19 +0100 Subject: [PATCH 045/103] added min slowness and min/maz backazimuth search ranges and made number of grid points parameters --- SeisSeeker/processing/detection.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index af8255f..6285d25 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -50,7 +50,9 @@ def xy_to_rtheta(x,y): @jit(nopython=True, parallel=True)#, nogil=True) -def _fast_freq_domain_array_proc(data, max_sl, fs, target_freqs, xx, yy, n_stations, n_t_samp, remove_autocorr): +def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n_baz, fs, target_freqs, xx, yy, + n_stations, n_t_samp, remove_autocorr, + ): """Function to perform array processing fast due to being designed to be wrapped using Numba. Function inspired by Bowden et al. (2021). Performs array processing in polar coordinates. @@ -60,25 +62,23 @@ def _fast_freq_domain_array_proc(data, max_sl, fs, target_freqs, xx, yy, n_stati # Define grid of slownesses: # number of pixes in x and y # (Determines number of phase shifts to perform) - n_ur = 26 #51 #101 - n_utheta = 120 #51 #101 - ur = np.linspace(0,max_sl,n_ur) - utheta = np.linspace(0,360-(360/n_utheta),n_utheta) + ur = np.linspace(min_sl,max_sl,n_sl, endpoint=True) + utheta = np.linspace(min_baz, max_baz, n_baz, endpoint=True) utheta_rad = np.deg2rad(utheta) dur=ur[1]-ur[0] dutheta=utheta[1]-utheta[0] # Compute time-shifts once: # (so that don't have to do it for every frequency) - tlib = np.zeros((n_stations,n_ur,n_utheta), dtype=np.complex128) - for ir in range(0,n_ur): - for itheta in range(0,n_utheta): + tlib = np.zeros((n_stations,n_sl,n_baz), dtype=np.complex128) + for ir in range(0,n_sl): + for itheta in range(0,n_baz): # tlib[:,ix,iy] = xx*ux[ix] + yy*uy[iy] # (distance x slowness = distance / velocity = time) tlib[:,ir,itheta] = xx*ur[ir]*np.sin((utheta_rad[itheta])) + yy*ur[ir]*np.cos((utheta_rad[itheta])) # (distance x slowness = distance / velocity = time) # Since receivers are relative to the array centre, can shift all receivers back to that centre. # Create data stores: - Pfreq_all = np.zeros((data.shape[0],len(target_freqs),n_ur,n_utheta), dtype=np.complex128) # Explicitly create Pxx_all, as otherwise prange won't work correctly. + Pfreq_all = np.zeros((data.shape[0],len(target_freqs),n_sl,n_baz), dtype=np.complex128) # Explicitly create Pxx_all, as otherwise prange won't work correctly. # Then loop over windows: for win_idx in prange(data.shape[0]): @@ -99,7 +99,7 @@ def _fast_freq_domain_array_proc(data, max_sl, fs, target_freqs, xx, yy, n_stati Pxx_all[:,sta_idx] = Pxx_curr # Loop over all freqs, performing phase shifts: - Pfreq=np.zeros((len(target_freqs),n_ur,n_utheta),dtype=np.complex128) + Pfreq=np.zeros((len(target_freqs),n_sl,n_baz),dtype=np.complex128) counter_grid = 0 for ii in range(len(target_freqs)): # Find closest current freq.: @@ -118,8 +118,8 @@ def _fast_freq_domain_array_proc(data, max_sl, fs, target_freqs, xx, yy, n_stati Rxx[i1,i2] = 0 # And loop over phase shifts, calculating cross-correlation power: - for ir in range(0,n_ur): - for itheta in range(0,n_utheta): + for ir in range(0,n_sl): + for itheta in range(0,n_baz): timeshifts = tlib[:,ir,itheta] # Calculate the "steering vector" (a vector in frequency space, based on phase-shift) a = np.exp(-1j*2*np.pi*target_f*timeshifts) # (a is a steering vector, to allign all traces with array centre) aconj = np.conj(a) @@ -752,7 +752,7 @@ def _load_day_of_data(self, year, month, day, hour=None): st.trim(starttime=self.starttime) if self.endtime < st[0].stats.endtime: st.trim(endtime=self.endtime) - return st + return st.normalize() def _convert_st_to_np_data(self, st): From 9c02fbc7bacace7661a793076b5af7319f523fa1 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 26 Apr 2024 16:37:41 +0100 Subject: [PATCH 046/103] removed un-used variables --- SeisSeeker/processing/detection.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 6285d25..db5cc2f 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -806,8 +806,6 @@ def _find_time_series(self, Psum_all): # Calcualte ux, uy: ur = np.linspace(0, self.max_sl,Psum_all.shape[1]) utheta = utheta = np.linspace(0,360-(360/Psum_all.shape[2]),Psum_all.shape[2]) - dur=ur[1]-ur[0] - dutheta=utheta[1]-utheta[0] # Create time-series: n_win_curr = Psum_all.shape[0] t_series = np.arange(self.win_step_inc_s/2,(n_win_curr*self.win_step_inc_s) + (self.win_step_inc_s/2), self.win_step_inc_s) From 7a4f713fb14cf1cbaa0b460ca186ebc240a23081 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 26 Apr 2024 16:46:35 +0100 Subject: [PATCH 047/103] added new class attribute defaults for min/max baz min slowness, n slow, n baz --- SeisSeeker/processing/detection.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index db5cc2f..db4d4e7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -531,7 +531,12 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo self.freqmin = None self.freqmax = None self.num_freqs = 100 + self.min_sl = 0 self.max_sl = 1.0 + self.n_sl = 51 + self.min_baz = 0 + self.max_baz = 360 + self.n_baz = 181 self.win_len_s = 0.1 self.win_step_inc_s = 0.1 # (Note: Default is to step with no overlap) self.remove_autocorr = True @@ -846,8 +851,8 @@ def _beamforming(self, st_trimmed, verbosity=0): if verbosity>1: print("Performing run for",data.shape[0],"windows") tic = time.time() - Pfreq_all = _fast_freq_domain_array_proc(data, self.max_sl, self.fs, target_freqs, xx, yy, - self.n_stations, self.n_t_samp, self.remove_autocorr) + Pfreq_all = _fast_freq_domain_array_proc(data, self.min_sl, self.max_sl, self.n_sl, self.min_baz, self.max_baz, self.n_baz, + self.fs, target_freqs, xx, yy, self.n_stations, self.n_t_samp, self.remove_autocorr) if verbosity>1: toc = time.time() print(toc-tic) From cf3944376c216ca0458fb05bf42d282457c9aedd Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 26 Apr 2024 16:50:11 +0100 Subject: [PATCH 048/103] removed endpoint kwarg, defaults to true anyway --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index db4d4e7..c65ebc7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -62,8 +62,8 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n # Define grid of slownesses: # number of pixes in x and y # (Determines number of phase shifts to perform) - ur = np.linspace(min_sl,max_sl,n_sl, endpoint=True) - utheta = np.linspace(min_baz, max_baz, n_baz, endpoint=True) + ur = np.linspace(min_sl,max_sl,n_sl) + utheta = np.linspace(min_baz, max_baz, n_baz) utheta_rad = np.deg2rad(utheta) dur=ur[1]-ur[0] dutheta=utheta[1]-utheta[0] From 1017adb03625d98af8786b8ce1ce5bd3986bae86 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 30 Apr 2024 09:46:26 +0100 Subject: [PATCH 049/103] made plotting of beamformed baz-slow space a function as it is used twice in the same larger function --- SeisSeeker/processing/detection.py | 60 ++++++++++++++---------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index c65ebc7..2d218b7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -880,6 +880,30 @@ def _calculate_mad(self, x, scale=1.4826): mad = np.median(np.abs(x - np.median(x))) return scale * mad + + def plot_polar_slowness_space(self, beam_power, event_phase_arr_time, component, log=False): + + fig = plt.figure() + ax = fig.add_subplot(111, projection='polar') + rad = np.linspace(self.min_sl, self.max_sl, beam_power.shape[0]) + azm = np.linspace(np.radians(self.min_baz), + np.radians(self.max_baz), beam_power.shape[1]) + th, r = np.meshgrid(azm, rad) + ax.set_theta_offset(np.pi/2) + ax.set_theta_direction(-1) + if log: + im = ax.pcolormesh(th, r, np.log(beam_power), cmap='magma') + else: + im = ax.pcolormesh(th, r, beam_power, cmap='magma') + + plt.colorbar(im) + plt.grid() + event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' + event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' + vesp_figpath = Path(self.outdir, 'plots', ' vespagrams') + vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist + fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_{component}.png', dpi=600) + plt.close() def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosity=0): """Function to calculate uncertainties for phase-associated event detections. @@ -980,24 +1004,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - fig = plt.figure() - Axes3D(fig) - rad = np.linspace(0, self.max_sl, Psum_opt.shape[0]) - azm = np.linspace(0, 2 * np.pi, Psum_opt.shape[1]) - th, r = np.meshgrid(azm, rad) - ax = plt.subplot(projection="polar") - ax.set_theta_offset(np.pi/2) - ax.set_theta_direction(-1) - im = ax.pcolormesh(th, r, Psum_opt/Psum_opt.max(), cmap='inferno') - plt.colorbar(im) - plt.grid() - event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' - event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' - vesp_figpath = Path(self.outdir, 'plots', ' vespagrams') - vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist - fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_vert.png', dpi=600) - plt.close() - + self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz', log=True) # ------- For horizontal -------: # And find FWHM for t2 pick: # (only use ascending currently (assume symetric pdf)) @@ -1070,19 +1077,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - fig = plt.figure() - Axes3D(fig) - rad = np.linspace(0, self.max_sl, Psum_opt.shape[0]) - azm = np.linspace(0, 2 * np.pi, Psum_opt.shape[1]) - th, r = np.meshgrid(azm, rad) - ax = plt.subplot(projection="polar") - ax.set_theta_offset(np.pi/2) - ax.set_theta_direction(-1) - im = ax.pcolormesh(th, r, Psum_opt, cmap='inferno') - plt.colorbar(im) - plt.grid() - fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_horz.png', dpi=600) - plt.close() + self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz', log=True) # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], 'slow2_err': [slow2_err], 'bazi1_err': [bazi1_err], 'bazi2_err': [bazi2_err]}) @@ -1097,8 +1092,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi events_df = pd.concat([events_df, uncertainties_df], axis=1) return events_df - - + def detect_events(self, verbosity=0, fnames=None): """Function to detect events, based on the power time-series generated by run_array_proc(). Note: Currently, only Median Absolute Deviation From 860bc0cb3888896d60288999056400fb90fad299 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 30 Apr 2024 09:58:15 +0100 Subject: [PATCH 050/103] removed default of log(power) for plotting of polar slowness space --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 2d218b7..793c7eb 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1004,7 +1004,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz', log=True) + self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz') # ------- For horizontal -------: # And find FWHM for t2 pick: # (only use ascending currently (assume symetric pdf)) @@ -1077,7 +1077,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz', log=True) + self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz') # And append data to overall uncertainties df: uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], 'slow2_err': [slow2_err], 'bazi1_err': [bazi1_err], 'bazi2_err': [bazi2_err]}) From 001bb83d09860e226ec29d2c88a98e52ae7f0f62 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 30 Apr 2024 17:54:32 +0100 Subject: [PATCH 051/103] remove space in vespagram path --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 793c7eb..8b5e9cc 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -900,7 +900,7 @@ def plot_polar_slowness_space(self, beam_power, event_phase_arr_time, component, plt.grid() event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' - vesp_figpath = Path(self.outdir, 'plots', ' vespagrams') + vesp_figpath = Path(self.outdir, 'plots', 'vespagrams') vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_{component}.png', dpi=600) plt.close() From 1a09bd741f5012e4144e9156e46a2bb642195808 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 14:40:55 +0100 Subject: [PATCH 052/103] detection_t_series name now set from hour in loop rtaher than stream starttime --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 8b5e9cc..e627eea 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -651,7 +651,7 @@ def run_array_proc(self): store_df.reset_index(drop=True, inplace=True) # And save data out: - outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{starttime_this_st.hour:02d}00_ch{self.channel_curr[-1]}.csv' + outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' out_fname = os.path.join(self.outdir, outfile) store_df.to_csv(out_fname, index=False) From 6c761b50ccec80670a585095f56ed459cb3552c6 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 14:55:24 +0100 Subject: [PATCH 053/103] switched to using datetime, tweaked flag for doing hourly/daily --- SeisSeeker/processing/detection.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index e627eea..8575de4 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -723,8 +723,15 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): print("="*60) - def _load_day_of_data(self, year, month, day, hour=None): - """Function to load a day of data.""" + def _load_data(self, data_datetime, whole_day=False): + """ + Function to load a day of data. + + """ + year = data_datetime.year + month = data_datetime.month + day = data_datetime.day + # Load in data: mseed_dir = Path(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) # print(mseed_dir) @@ -733,17 +740,17 @@ def _load_day_of_data(self, year, month, day, hour=None): # [J Asplet - think about replacing station DataFrame with StatonXML object] station = row['Name'] for channel in self.channels_to_use: - if hour: - timestamp = f'{year:02d}{month:02d}{day:02d}T{hour:02d}0000' + if whole_day: + timestamp = f'{year:02d}{month:02d}{day:02d}T*' else: - timestamp = f'{year:02d}{month:02d}{day:02d}T*' + hour = data_datetime.hour + timestamp = f'{year:02d}{month:02d}{day:02d}T{hour:02d}0000' try: st_tmp = obspy.read(f'{mseed_dir}/{timestamp}_{station}_{channel}.mseed') for tr in st_tmp: st.append(tr) except: print(f"No data for {station}, channel = {channel}, timestamp {timestamp}. Skipping this data.") - # print(full_fname) continue # Merge data: st.detrend('demean') @@ -752,7 +759,7 @@ def _load_day_of_data(self, year, month, day, hour=None): if self.freqmin: if self.freqmax: st.filter('bandpass', freqmin=self.freqmin, freqmax=self.freqmax) - # And trim data, if some lies outside start and end times: + # And trim data, if some lies outside start and end time of beamforming period: if self.starttime > st[0].stats.starttime: st.trim(starttime=self.starttime) if self.endtime < st[0].stats.endtime: From 13370ff9075671111b260c5ffd10c01907a57290 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:03:47 +0100 Subject: [PATCH 054/103] removed datetime for compatability. now test if hour is None not if hour. updated docstring --- SeisSeeker/processing/detection.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 8575de4..59ff743 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -723,15 +723,28 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): print("="*60) - def _load_data(self, data_datetime, whole_day=False): + def _load_data(self, year, month, day, hour=None): """ - Function to load a day of data. + Function to load data. If no hour is specified the whole day will be read in. + Otherwise the hour of data will be loaded. + + Parameters + ---------- + + year : int + year to load data for (yyyy) + month : int + month to load data for. Leading 0's will be added. + day : int + day to load data for. Leading 0's will be added. + hour : int, Optional + hour to load data for. Leading 0's will be added. + Returns: + ---------- + data : obspy.Stream + Waveform data for requested date and time. """ - year = data_datetime.year - month = data_datetime.month - day = data_datetime.day - # Load in data: mseed_dir = Path(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) # print(mseed_dir) @@ -740,10 +753,9 @@ def _load_data(self, data_datetime, whole_day=False): # [J Asplet - think about replacing station DataFrame with StatonXML object] station = row['Name'] for channel in self.channels_to_use: - if whole_day: + if hour is None: timestamp = f'{year:02d}{month:02d}{day:02d}T*' else: - hour = data_datetime.hour timestamp = f'{year:02d}{month:02d}{day:02d}T{hour:02d}0000' try: st_tmp = obspy.read(f'{mseed_dir}/{timestamp}_{station}_{channel}.mseed') From 851034cb8fd979865ac1dbb46c90ad14a700f56f Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:05:07 +0100 Subject: [PATCH 055/103] updated function name --- SeisSeeker/processing/detection.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 59ff743..43c1712 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -593,7 +593,7 @@ def run_array_proc(self): store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) # Load data: - st = self._load_day_of_data(year=date.year, month=date.month, day=date.day, hour=hour) + st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) try: starttime_this_st = st[0].stats.starttime except IndexError: @@ -949,7 +949,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # (done like this to avoid unnneccessary read ins, improving eff.) event_phase_arr_time = obspy.UTCDateTime(row['t1']) if count == 0: - st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.month, event_phase_arr_time.day, hour=event_phase_arr_time.hour) + st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, + event_phase_arr_time.day, hour=event_phase_arr_time.hour) # Find uncertainties: # ------- For vertical -------: @@ -971,7 +972,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_phase_arr_time = obspy.UTCDateTime(row['t1']) # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: - st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.month, event_phase_arr_time.day, hour=event_phase_arr_time.hour) + st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, + event_phase_arr_time.day, hour=event_phase_arr_time.hour) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1042,7 +1044,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_phase_arr_time = obspy.UTCDateTime(row['t2']) # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: - st = self._load_day_of_data(event_phase_arr_time.year, event_phase_arr_time.month, event_phase_arr_time.day, hour=event_phase_arr_time.hour) + st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, + event_phase_arr_time.day, hour=event_phase_arr_time.hour) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1407,7 +1410,8 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo If True, returns st and composite_st. Optional. Default = False. """ # Load in raw mseed data: - st = self._load_day_of_data(arrival_time.year, arrival_time.month, arrival_time.day, hour=arrival_time.hour) + st = self._load_data(arrival_time.year, arrival_time.month, + arrival_time.day, hour=arrival_time.hour) # And trim data: st.trim(starttime=arrival_time-t_before_s, endtime=arrival_time+t_after_s) From f8d860433f9b0c33894ca3bfb1684cf8036b8df5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:07:45 +0100 Subject: [PATCH 056/103] make archivedir a Path object moved outfile (so we can test if it exists) --- SeisSeeker/processing/detection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 43c1712..cae3f30 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -512,7 +512,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo """ # Initialise input params: - self.archivedir = archivedir + self.archivedir = Path(archivedir) self.outdir = outdir self.stations_fname = stations_fname self.starttime = starttime @@ -584,6 +584,9 @@ def run_array_proc(self): for hour in range(24): # Loop over every hour in every day.. print(f"Processing for hour: {hour:02d}") + # Make outfiles + outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' + if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): From 1473c6f0179dac610b12bc96e846a758852d0cfb Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:15:42 +0100 Subject: [PATCH 057/103] removed some prints and added test for if files exist in given archivedir --- SeisSeeker/processing/detection.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index cae3f30..3eefed7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -570,9 +570,7 @@ def run_array_proc(self): dt_start = self.starttime.date dt_end = self.endtime.date ndays = (dt_end - dt_start).days + 1 - print(ndays) query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] - print(query_dates) for date in query_dates: # Loop over dates within start/end range: # Loop over channels: @@ -584,9 +582,12 @@ def run_array_proc(self): for hour in range(24): # Loop over every hour in every day.. print(f"Processing for hour: {hour:02d}") - # Make outfiles + # Make outfile outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' - + if ((self.archivedir / outfile).is_file()) & (self.overwrite): + print(f'{outfile} exists in {self.archivedir}') + print('Move to next hour') + continue if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): From e0f5134408b21e9ed22fea47ebbcd5b6d4ef03e5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:21:45 +0100 Subject: [PATCH 058/103] fixed bug in directory for testing of detection_t_series exists --- SeisSeeker/processing/detection.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 3eefed7..1bad8f5 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -584,7 +584,7 @@ def run_array_proc(self): print(f"Processing for hour: {hour:02d}") # Make outfile outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' - if ((self.archivedir / outfile).is_file()) & (self.overwrite): + if ((self.outdir / outfile).is_file()) & (self.overwrite): print(f'{outfile} exists in {self.archivedir}') print('Move to next hour') continue @@ -593,8 +593,8 @@ def run_array_proc(self): if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): continue - # Create datastore: - store_df = pd.DataFrame({'t': [], 'power': [], 'slowness': [], 'back_azi': []}) + # Create datastores: + data_store = {'t': [], 'power': [], 'slowness': [], 'back_azi': []} # Load data: st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) @@ -645,18 +645,19 @@ def run_array_proc(self): for t_serie in t_series: t_series_out.append( str(time_this_minute_st + t_serie) ) - tmp_df = pd.DataFrame({'t': t_series_out, 'power': powers, 'slowness': slownesses, 'back_azi': back_azis}) - store_df = pd.concat([store_df, tmp_df]) + data_store['t'].append(t_series_out) + data_store['power'].append(powers) + data_store['slowness'].append(slownesses) + data_store['back_azi'].append(back_azis) # And clear memory: del Psum_all, t_series, powers, slownesses, back_azis gc.collect() - store_df.reset_index(drop=True, inplace=True) - # And save data out: - outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' out_fname = os.path.join(self.outdir, outfile) + #make DataFrame "just-in-time" as it is more efficient this way + store_df = pd.DataFrame(data_store) store_df.to_csv(out_fname, index=False) # And append fname to history: From ee5d65aa86b149209d84ef399f2320ee8a5c9955 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:26:59 +0100 Subject: [PATCH 059/103] added note that a better check for if data is loaded is needed --- SeisSeeker/processing/detection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 1bad8f5..720a116 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -600,6 +600,7 @@ def run_array_proc(self): st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) try: starttime_this_st = st[0].stats.starttime + #need a better test for this except IndexError: # And skip if no data: print("Skipping hour as no data") From cf8a3c4fdee631d844faeb9ee79adcfea08f6c91 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:28:07 +0100 Subject: [PATCH 060/103] improved readability --- SeisSeeker/processing/detection.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 720a116..1ebb50f 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -627,8 +627,16 @@ def run_array_proc(self): else: self.win_pad_s = 0. if self.endtime - self.starttime > 60: - st_trimmed.trim(starttime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute), - endtime=obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute)+60+self.win_pad_s) + st_trimmed.trim(starttime=obspy.UTCDateTime(year=date.year, + month=date.month, + day=date.day, + hour=hour, + minute=minute), + endtime=obspy.UTCDateTime(year=date.year, + month=date.month, + day=date.day, + hour=hour, + minute=minute)+60+self.win_pad_s) else: st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) time_this_minute_st = st_trimmed[0].stats.starttime From 8a50b0c8f017de47fd5a44e416354bb4c8a81031 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:29:03 +0100 Subject: [PATCH 061/103] removed trailing blank lines --- SeisSeeker/processing/detection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 1ebb50f..06c6b3d 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -671,9 +671,7 @@ def run_array_proc(self): # And append fname to history: self.out_fnames_array_proc.append(out_fname) - - - + return None def _setup_array_receiver_coords(self): From 7054002fc22939e220214d0655997e56dea1b886 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:32:54 +0100 Subject: [PATCH 062/103] removed some trailing lines --- SeisSeeker/processing/detection.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 3eefed7..922e264 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -168,7 +168,6 @@ def _phase_associator_core_worker(peaks_Z, peaks_hor, bazis_Z, bazis_hor, bazi_t return Z_hor_phase_pair_idxs - def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_tol, filt_phase_assoc_by_max_power, max_phase_sep_s, min_event_sep_s, verbosity=0): """ Function to perform phase association for numba implementation. @@ -385,7 +384,6 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): return composite_st - class setup_detection: """ Class to create detection object, for running array detection algorithm. @@ -808,7 +806,6 @@ def _convert_st_to_np_data(self, st): data[i,j,:] = 0. return data - def _stack_results(self, Pfreq_all): """Function to perform stacking of the results.""" Psum_all = np.zeros((Pfreq_all.shape[0], Pfreq_all.shape[2], Pfreq_all.shape[3]), dtype=complex) @@ -821,7 +818,6 @@ def _stack_results(self, Pfreq_all): Psum_all[i,:,:] = np.sum(Pfreq_all[i,:,:,:],axis=0) return Psum_all - def _find_time_series(self, Psum_all): """Function to calculate beamforming time-series outputs, given a raw beamforming result. @@ -903,7 +899,6 @@ def _calculate_mad(self, x, scale=1.4826): mad = np.median(np.abs(x - np.median(x))) return scale * mad - def plot_polar_slowness_space(self, beam_power, event_phase_arr_time, component, log=False): fig = plt.figure() From eceb3a858abf4f3592ff0217cf823d5593b2c471 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:37:55 +0100 Subject: [PATCH 063/103] fixed issue where self.outdir was not Path object --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 845ade6..20d9de1 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -511,7 +511,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo """ # Initialise input params: self.archivedir = Path(archivedir) - self.outdir = outdir + self.outdir = Path(outdir) self.stations_fname = stations_fname self.starttime = starttime self.endtime = endtime From c1d473155c5978b40b7ff8db080e64d8269ca589 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:40:25 +0100 Subject: [PATCH 064/103] changed self.overwrite to self.skip_existing --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 20d9de1..459d761 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -582,7 +582,7 @@ def run_array_proc(self): print(f"Processing for hour: {hour:02d}") # Make outfile outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' - if ((self.outdir / outfile).is_file()) & (self.overwrite): + if ((self.outdir / outfile).is_file()) & (self.skip_existing): print(f'{outfile} exists in {self.archivedir}') print('Move to next hour') continue From d43f6b5820898fb94d0fe7760c7212cb548f77fb Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 15:42:36 +0100 Subject: [PATCH 065/103] put inital load_data inside try statement. --- SeisSeeker/processing/detection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 459d761..139c61c 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -595,10 +595,8 @@ def run_array_proc(self): data_store = {'t': [], 'power': [], 'slowness': [], 'back_azi': []} # Load data: - st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) try: - starttime_this_st = st[0].stats.starttime - #need a better test for this + st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) except IndexError: # And skip if no data: print("Skipping hour as no data") From 6eea47e36810abd17c5163ab13ebb85bb5b3d55e Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 16:23:37 +0100 Subject: [PATCH 066/103] repalce append with extend as we are adding lists to lists (and want a flat list out) --- SeisSeeker/processing/detection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 139c61c..30f566f 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -650,10 +650,10 @@ def run_array_proc(self): for t_serie in t_series: t_series_out.append( str(time_this_minute_st + t_serie) ) - data_store['t'].append(t_series_out) - data_store['power'].append(powers) - data_store['slowness'].append(slownesses) - data_store['back_azi'].append(back_azis) + data_store['t'].extend(t_series_out) + data_store['power'].extend(powers) + data_store['slowness'].extend(slownesses) + data_store['back_azi'].extend(back_azis) # And clear memory: del Psum_all, t_series, powers, slownesses, back_azis From c5b846f28b69ba1bd6e348b2c7321e497f3358fc Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 7 May 2024 17:19:09 +0100 Subject: [PATCH 067/103] swithced from printing to logging --- SeisSeeker/processing/detection.py | 51 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 30f566f..d5d88c7 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -20,13 +20,14 @@ from scipy.signal import find_peaks from numba import jit, objmode, prange, set_num_threads import gc +import logging # import multiprocessing as mp import time import glob import pickle from SeisSeeker.processing import lookup_table_manager, location - +logger = logging.getLogger(__name__) #----------------------------------------------- Define main functions ----------------------------------------------- class CustomError(Exception): @@ -182,7 +183,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # Perform core phae association: # Prep. data for numba format: if verbosity > 1: - print("Pre-processing time-series") + logger.info("Pre-processing time-series") t_Z_secs_after_start = [] for index, row in t_series_df_Z.iterrows(): t_Z_secs_after_start.append(obspy.UTCDateTime(row['t']) - obspy.UTCDateTime(t_series_df_Z['t'][0])) @@ -191,11 +192,11 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t t_hor_secs_after_start.append(obspy.UTCDateTime(row['t']) - obspy.UTCDateTime(t_series_df_hor['t'][0])) # Run function: if verbosity > 1: - print("Performing phase association") + logger.info("Performing phase association") Z_hor_phase_pair_idxs = _phase_associator_core_worker(peaks_Z, peaks_hor, bazis_Z, bazis_hor, bazi_tol, t_Z_secs_after_start, t_hor_secs_after_start, max_phase_sep_s) # Organise outputs into useful form: if verbosity > 1: - print("Writing events") + logger.info("Writing events") curr_events = {'t1':[],'t2':[], 'pow1': [], 'pow2':[], 'slow1':[], 'slow2':[], 'bazi1':[], 'bazi2':[]} @@ -415,6 +416,9 @@ class setup_detection: Attributes ---------- + skip_existing : float + If True, then skip exisitng detection time series in outdir. Default is True. + freqmin : float If specified, lower frequency of bandpass filter, in Hz. Default is None. @@ -526,6 +530,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo # And define attributes: # For array processing: + self.skip_existing = True self.freqmin = None self.freqmax = None self.num_freqs = 100 @@ -573,18 +578,17 @@ def run_array_proc(self): # Loop over dates within start/end range: # Loop over channels: for self.channel_curr in self.channels_to_use: - print("="*60) - print(f"Processing data for day {date}, channel {self.channel_curr}") + logger.info("="*60) + logger.info(f"Processing data for day {date}, channel {self.channel_curr}") # And process for individual hours: # (to reduce memory usage) for hour in range(24): # Loop over every hour in every day.. - print(f"Processing for hour: {hour:02d}") # Make outfile outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' if ((self.outdir / outfile).is_file()) & (self.skip_existing): - print(f'{outfile} exists in {self.archivedir}') - print('Move to next hour') + logger.warning(f'{outfile} exists in {self.archivedir}') + logger.warning('Move to next hour') continue if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue @@ -599,7 +603,7 @@ def run_array_proc(self): st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) except IndexError: # And skip if no data: - print("Skipping hour as no data") + logger.exception("Skipping hour as no data") del st gc.collect() continue @@ -644,7 +648,6 @@ def run_array_proc(self): # Calculate time-series outputs (for detection) from data: t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) - # And append to data out: t_series_out = [] for t_serie in t_series: @@ -770,7 +773,7 @@ def _load_data(self, year, month, day, hour=None): for tr in st_tmp: st.append(tr) except: - print(f"No data for {station}, channel = {channel}, timestamp {timestamp}. Skipping this data.") + logger.exception(f"No data for {station}, channel = {channel}, timestamp {timestamp}. Skipping this data.") continue # Merge data: st.detrend('demean') @@ -806,7 +809,7 @@ def _convert_st_to_np_data(self, st): else: # Zero pad data (as insufficient data passed for final window) and print warning: data[i,j,:] = 0. - print("Warning: Zero-padding as not enough data to fill window overlap ( for win_len_s =", self.win_len_s, "and win_step_inc_s =", self.win_step_inc_s, ")") + logger.warning("Warning: Zero-padding as not enough data to fill window overlap ( for win_len_s =", self.win_len_s, "and win_step_inc_s =", self.win_step_inc_s, ")") except IndexError: # Deal with if a particular station has no data for given window: data[i,j,:] = 0. @@ -874,13 +877,13 @@ def _beamforming(self, st_trimmed, verbosity=0): yy = self.stations_df['y_array_coords_km'].values # And run: if verbosity>1: - print("Performing run for",data.shape[0],"windows") + logger.info("Performing run for",data.shape[0],"windows") tic = time.time() Pfreq_all = _fast_freq_domain_array_proc(data, self.min_sl, self.max_sl, self.n_sl, self.min_baz, self.max_baz, self.n_baz, self.fs, target_freqs, xx, yy, self.n_stations, self.n_t_samp, self.remove_autocorr) if verbosity>1: toc = time.time() - print(toc-tic) + logger.info(f'runtime for _beamforming is {toc-tic}') # And tidy: del data gc.collect() @@ -949,7 +952,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi for index, row in events_df.iterrows(): if count % 10 == 0: if verbosity > 0: - print("Calculating uncertainty for event", count+1, "/", len(events_df)) + logger.info("Calculating uncertainty for event", count+1, "/", len(events_df)) # Load in data (if needed): # (done like this to avoid unnneccessary read ins, improving eff.) event_phase_arr_time = obspy.UTCDateTime(row['t1']) @@ -1167,9 +1170,9 @@ def detect_events(self, verbosity=0, fnames=None): # Check if all inputs are same length, and if not, skip file: if not len(t_series_df_Z) == len(t_series_df_N) == len(t_series_df_E): - print("Warning: Files with f uid", f_uid, + logger.warning("Warning: Files with f uid", f_uid, "are not of equal length. Therefore using shortest length (will miss some data).") - print("( Lengths are", len(t_series_df_Z) , len(t_series_df_N) , len(t_series_df_E), ")") + logger.warning("( Lengths are", len(t_series_df_Z) , len(t_series_df_N) , len(t_series_df_E), ")") min_len = np.min(np.array([len(t_series_df_Z), len(t_series_df_N), len(t_series_df_E)])) t_series_df_Z = t_series_df_Z.iloc[:min_len] t_series_df_N = t_series_df_N.iloc[:min_len] @@ -1188,7 +1191,7 @@ def detect_events(self, verbosity=0, fnames=None): t_series_df_hor["back_azi"] = np.average(np.vstack((t_series_df_N['back_azi'], t_series_df_E['back_azi'])), axis=0, weights=np.vstack((N_weighting, E_weighting))) # Weighted mean (weighted by power) - print("(Weighted horizontal slowness and back-azi using power)") + # print("(Weighted horizontal slowness and back-azi using power)") del N_weighting, E_weighting, t_series_df_N, t_series_df_E gc.collect() @@ -1214,10 +1217,10 @@ def detect_events(self, verbosity=0, fnames=None): # Plot detected, phase-associated picks: if verbosity > 1: - print("="*40) - print("Event phase associations:") - print(events_df) - print("="*40) + # print("="*40) + logger.info("Event phase associations:") + # print(events_df) + # print("="*40) fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(9,6)) # Plot power: ax[0].plot(t_series_df_Z['t'], t_series_df_Z['power'], label="Vertical power") @@ -1232,7 +1235,7 @@ def detect_events(self, verbosity=0, fnames=None): ax[0].scatter(events_df_all['t1'], np.ones(len(events_df_all))*np.max(t_series_df_Z['power']), c='r', label="P phase picks") ax[0].scatter(events_df_all['t2'], np.ones(len(events_df_all))*np.max(t_series_df_Z['power']), c='b', label="S phase picks") else: - print("No events to plot.") + logger.info("No events to plot.") ax[0].legend() ax[2].set_xlabel("Time") ax[0].set_ylabel("Power (arb. units)") From 2a22422dee64dc8b483863fdef221336fb4290c9 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 24 May 2024 16:39:09 +0200 Subject: [PATCH 068/103] fixed log --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index d5d88c7..37c079d 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -587,7 +587,7 @@ def run_array_proc(self): # Make outfile outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' if ((self.outdir / outfile).is_file()) & (self.skip_existing): - logger.warning(f'{outfile} exists in {self.archivedir}') + logger.warning(f'{outfile} exists in {self.outdir}') logger.warning('Move to next hour') continue if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: @@ -1033,7 +1033,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz') + self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='vert') # ------- For horizontal -------: # And find FWHM for t2 pick: # (only use ascending currently (assume symetric pdf)) From afb96248f5414082ab944e9867292d5f18c45c51 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 31 May 2024 14:35:58 +0100 Subject: [PATCH 069/103] fixed bug where seisseeker will search over 2 days instead of 1 --- SeisSeeker/processing/detection.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 37c079d..f10d9c0 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -572,7 +572,12 @@ def run_array_proc(self): # Find number of days to run array processing over dt_start = self.starttime.date dt_end = self.endtime.date - ndays = (dt_end - dt_start).days + 1 + if (dt_end - dt_start).days == 0: + #round up to one day at minimum + ndays = 1 + else: + ndays = (dt_end - dt_start).days + query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] for date in query_dates: # Loop over dates within start/end range: From 9ebe31380432cf159a45c8cd256b31dd8af6be95 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 31 May 2024 14:45:41 +0100 Subject: [PATCH 070/103] fixed logging bug --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index f10d9c0..c593f6c 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -814,7 +814,7 @@ def _convert_st_to_np_data(self, st): else: # Zero pad data (as insufficient data passed for final window) and print warning: data[i,j,:] = 0. - logger.warning("Warning: Zero-padding as not enough data to fill window overlap ( for win_len_s =", self.win_len_s, "and win_step_inc_s =", self.win_step_inc_s, ")") + logger.warning(f"Warning: Zero-padding as not enough data to fill window overlap ( for win_len_s = {self.win_len_s}, and win_step_inc_s = {self.win_step_inc_s})") except IndexError: # Deal with if a particular station has no data for given window: data[i,j,:] = 0. From 063bebdeefc1805f71b4dc12689bcf003780e8bc Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 31 May 2024 16:00:24 +0100 Subject: [PATCH 071/103] added phase wieghted stacking function and requirement (hibert from scipy) --- SeisSeeker/processing/detection.py | 47 ++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index c593f6c..691ecbd 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -17,7 +17,7 @@ import os, sys import obspy import datetime -from scipy.signal import find_peaks +from scipy.signal import find_peaks, hilbert from numba import jit, objmode, prange, set_num_threads import gc import logging @@ -385,6 +385,41 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): return composite_st +def _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree=1): + """Function to create stacked data st.""" + + Z_inst_phase_all = hilbert(Z_all) + N_inst_phase_all = hilbert(N_all) + E_inst_phase_all = hilbert(E_all) + + Z_phase_stack = (np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1), axis=1)) + N_phase_stack = (np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1), axis=1)) + E_phase_stack = (np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1), axis=1)) + + composite_st = obspy.Stream() + # For Z stacked: + tr = st[0].copy() + tr.stats.station = "PW_STACK" + tr.stats.channel = st[0].stats.channel[0:2]+"Z" + tr.data = np.mean(Z_all, axis=1)*(Z_phase_stack**degree) + composite_st.append(tr) + # For N stacked: + tr = st[0].copy() + tr.stats.station = "PW-STACK" + tr.stats.channel = st[0].stats.channel[0:2]+"N" + tr.data = np.mean(N_all, axis=1)*(N_phase_stack**degree) + composite_st.append(tr) + # For E stacked: + tr = st[0].copy() + tr.stats.station = "PW-STACK" + tr.stats.channel = st[0].stats.channel[0:2]+"E" + tr.data = np.mean(E_all, axis=1)*(E_phase_stack**degree) + composite_st.append(tr) + del tr + gc.collect() + + return composite_st + class setup_detection: """ Class to create detection object, for running array detection algorithm. @@ -1392,7 +1427,7 @@ def load(self, preload_fname): f.close() print("Loaded detection instance from:", preload_fname) - + def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slows_1_2, t_before_s=10, t_after_s=10, st_out_fname='out.m', return_streams=False): """Function to find array stacked stream from back-azimuth and slowness. Returns average amplitude time-series seismogram of stacked array data, for all three componets. @@ -1458,10 +1493,8 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo n_stat = len(st.select(channel="??Z")) # Get unique channels: chan_labels_tmp = [] - chan_labels_unique = [] for tr in st: chan_labels_tmp.append(tr.stats.channel) - chan_labels_unique = list(set(chan_labels_tmp)) # Create datastores to save to: max_st_len = 0 for i in range(len(st)): @@ -1503,8 +1536,10 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo E_all[:len(st.select(channel="??2")[i].data),i] = st.select(channel="??2")[i].data # And create stacked data stream: - composite_st = _create_stacked_data_st(st, Z_all, N_all, E_all) - + if method == 'linear': + composite_st = _create_stacked_data_st(st, Z_all, N_all, E_all) + elif method == 'pws': + composite_s = _create_phase_weighted_stack_st(st, Z_all, N_all, E_all) # And decimate data back down to original sampling rate: st.decimate(10, no_filter=True) composite_st.decimate(10, no_filter=True) From 7ad300f18648e1755b2ec7faeee65c6bda0705ef Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 31 May 2024 17:02:07 +0100 Subject: [PATCH 072/103] fixed PW-Stack typo --- SeisSeeker/processing/detection.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 691ecbd..b0c2747 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -388,20 +388,23 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): def _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree=1): """Function to create stacked data st.""" - Z_inst_phase_all = hilbert(Z_all) - N_inst_phase_all = hilbert(N_all) - E_inst_phase_all = hilbert(E_all) - - Z_phase_stack = (np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1), axis=1)) - N_phase_stack = (np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1), axis=1)) - E_phase_stack = (np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1), axis=1)) - + print(Z_all.shape) + Z_inst_phase_all = hilbert(Z_all, axis=1) + N_inst_phase_all = hilbert(N_all, axis=1) + E_inst_phase_all = hilbert(E_all, axis=1) + print(Z_inst_phase_all.shape) + + Z_phase_stack = (np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1))) + N_phase_stack = (np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1))) + E_phase_stack = (np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1))) + print(Z_phase_stack.shape) composite_st = obspy.Stream() # For Z stacked: tr = st[0].copy() - tr.stats.station = "PW_STACK" + tr.stats.station = "PW-STACK" tr.stats.channel = st[0].stats.channel[0:2]+"Z" tr.data = np.mean(Z_all, axis=1)*(Z_phase_stack**degree) + print(tr.data.shape) composite_st.append(tr) # For N stacked: tr = st[0].copy() @@ -1428,7 +1431,7 @@ def load(self, preload_fname): print("Loaded detection instance from:", preload_fname) - def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slows_1_2, t_before_s=10, t_after_s=10, st_out_fname='out.m', return_streams=False): + def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slows_1_2, t_before_s=10, t_after_s=10, st_out_fname='out.m', return_streams=False, method='linear', degree=1): """Function to find array stacked stream from back-azimuth and slowness. Returns average amplitude time-series seismogram of stacked array data, for all three componets. Parameters @@ -1539,7 +1542,10 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo if method == 'linear': composite_st = _create_stacked_data_st(st, Z_all, N_all, E_all) elif method == 'pws': - composite_s = _create_phase_weighted_stack_st(st, Z_all, N_all, E_all) + composite_st = _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree) + elif method == 'nth_root': + pass + # composite_st = _create_nth_root_stack_st(st, Z_all, N_all, E_all, degree) # And decimate data back down to original sampling rate: st.decimate(10, no_filter=True) composite_st.decimate(10, no_filter=True) From c22acf37d0587034ab7902e324a2a71644f32ddf Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 3 Jun 2024 15:02:30 +0100 Subject: [PATCH 073/103] tweak to add exisitng detection t_series to outfnames (i.e so if you want to redo a crashed date rang ethen you can incluce the finsihed beamform t_series in the detection --- SeisSeeker/processing/detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index b0c2747..1e036d8 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -632,6 +632,7 @@ def run_array_proc(self): if ((self.outdir / outfile).is_file()) & (self.skip_existing): logger.warning(f'{outfile} exists in {self.outdir}') logger.warning('Move to next hour') + self.out_fnames_array_proc.append(self.outdir / outfile) continue if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue @@ -820,6 +821,7 @@ def _load_data(self, year, month, day, hour=None): continue # Merge data: st.detrend('demean') + st.detrend('linear') st.merge(method=1, fill_value=0.) # And apply filter: if self.freqmin: From 7202ac558f8132de5e9b015da3f940b220edb2f5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 4 Jun 2024 13:59:23 +0100 Subject: [PATCH 074/103] added logger messages for if detection t series is not read --- SeisSeeker/processing/detection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 1e036d8..5d03f03 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1204,6 +1204,8 @@ def detect_events(self, verbosity=0, fnames=None): fname_E = os.path.join(self.outdir, ''.join(( "detection_t_series_", f_uid, "_ch2.csv" ))) t_series_df_E = pd.read_csv(fname_E) else: + logger.warning(f'fname {fname} not in fname_array_proc list') + logger.debug(f'fname_array_proc first entry looks like this {self.out_fnames_array_proc[0]}') continue # Skip file, as not previously been processed. # And check to see that t-series exists within file: if len(t_series_df_Z) == 0: @@ -1251,7 +1253,8 @@ def detect_events(self, verbosity=0, fnames=None): # Phase assoicate by BAZI threshold and max. power: events_df = _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, - self.bazi_tol, self.filt_phase_assoc_by_max_power, self.max_phase_sep_s, self.min_event_sep_s, verbosity=verbosity) + self.bazi_tol, self.filt_phase_assoc_by_max_power, + self.max_phase_sep_s, self.min_event_sep_s, verbosity=verbosity) # Find uncertainties (in time, bazi, slowness): if self.calc_uncertainties: From f73a7dad2e32c5262ab2673bf11406c5571b0d8d Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 4 Jun 2024 16:54:33 +0100 Subject: [PATCH 075/103] taken out normalisation when loading data.. --- SeisSeeker/processing/detection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 5d03f03..66703ae 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -832,8 +832,7 @@ def _load_data(self, year, month, day, hour=None): st.trim(starttime=self.starttime) if self.endtime < st[0].stats.endtime: st.trim(endtime=self.endtime) - return st.normalize() - + return st def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" From e684e67e371b4781e69146f5b416e10885384555 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 4 Jun 2024 17:05:10 +0100 Subject: [PATCH 076/103] tweak to adding file name if exisits --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 66703ae..f5fed12 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -632,7 +632,7 @@ def run_array_proc(self): if ((self.outdir / outfile).is_file()) & (self.skip_existing): logger.warning(f'{outfile} exists in {self.outdir}') logger.warning('Move to next hour') - self.out_fnames_array_proc.append(self.outdir / outfile) + self.out_fnames_array_proc.append(f'{self.outdir} / {outfile}') continue if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue @@ -1204,7 +1204,7 @@ def detect_events(self, verbosity=0, fnames=None): t_series_df_E = pd.read_csv(fname_E) else: logger.warning(f'fname {fname} not in fname_array_proc list') - logger.debug(f'fname_array_proc first entry looks like this {self.out_fnames_array_proc[0]}') + logger.warning(f'fname_array_proc first entry looks like this {self.out_fnames_array_proc[0]}') continue # Skip file, as not previously been processed. # And check to see that t-series exists within file: if len(t_series_df_Z) == 0: From 362efdb6ae77712112cfa41421e79fdfcd9f8d29 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 6 Jun 2024 09:59:50 +0100 Subject: [PATCH 077/103] fix bug in assigning existing filenames to fnames array proc --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index f5fed12..3c45fc0 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -632,7 +632,7 @@ def run_array_proc(self): if ((self.outdir / outfile).is_file()) & (self.skip_existing): logger.warning(f'{outfile} exists in {self.outdir}') logger.warning('Move to next hour') - self.out_fnames_array_proc.append(f'{self.outdir} / {outfile}') + self.out_fnames_array_proc.append(f'{self.outdir}/{outfile}') continue if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: continue @@ -832,7 +832,7 @@ def _load_data(self, year, month, day, hour=None): st.trim(starttime=self.starttime) if self.endtime < st[0].stats.endtime: st.trim(endtime=self.endtime) - return st + return st.normalize() def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" From 94f43f144df4bb39f452efa4f545cb6ae595ec59 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 7 Jun 2024 13:56:32 +0100 Subject: [PATCH 078/103] removed normalised --- SeisSeeker/processing/detection.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 3c45fc0..1cb23dd 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -644,7 +644,8 @@ def run_array_proc(self): # Load data: try: - st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) + st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour, + norm=self.normalise_data) except IndexError: # And skip if no data: logger.exception("Skipping hour as no data") @@ -778,7 +779,7 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): print("="*60) - def _load_data(self, year, month, day, hour=None): + def _load_data(self, year, month, day, hour=None, norm=True): """ Function to load data. If no hour is specified the whole day will be read in. Otherwise the hour of data will be loaded. @@ -828,12 +829,16 @@ def _load_data(self, year, month, day, hour=None): if self.freqmax: st.filter('bandpass', freqmin=self.freqmin, freqmax=self.freqmax) # And trim data, if some lies outside start and end time of beamforming period: + print(self.starttime, st[0].stats.starttime) + print(self.endtime, st[0].stats.endtime) if self.starttime > st[0].stats.starttime: st.trim(starttime=self.starttime) if self.endtime < st[0].stats.endtime: st.trim(endtime=self.endtime) - return st.normalize() - + + else: + return st + def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" self.n_win = int(((st[0].stats.endtime - self.win_pad_s) - st[0].stats.starttime) / self.win_step_inc_s) # (Note: endtime - self.win_pad_s as pass extra padding via trimmed st) @@ -1002,7 +1007,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_phase_arr_time = obspy.UTCDateTime(row['t1']) if count == 0: st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour) + event_phase_arr_time.day, hour=event_phase_arr_time.hour, + norm=self.normalise_data) # Find uncertainties: # ------- For vertical -------: @@ -1025,7 +1031,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour) + event_phase_arr_time.day, hour=event_phase_arr_time.hour, + norm=self.normalise_data) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1097,7 +1104,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour) + event_phase_arr_time.day, hour=event_phase_arr_time.hour, + norm=self.normalise_data) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1466,7 +1474,7 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo """ # Load in raw mseed data: st = self._load_data(arrival_time.year, arrival_time.month, - arrival_time.day, hour=arrival_time.hour) + arrival_time.day, hour=arrival_time.hour, norm=False) # And trim data: st.trim(starttime=arrival_time-t_before_s, endtime=arrival_time+t_after_s) @@ -1541,7 +1549,9 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo E_all[:,i] = st.select(channel="??2")[i].data else: E_all[:len(st.select(channel="??2")[i].data),i] = st.select(channel="??2")[i].data - + except: + print('Date gap, continue... for now') + continue # And create stacked data stream: if method == 'linear': composite_st = _create_stacked_data_st(st, Z_all, N_all, E_all) From 1cd5ef5b83d40d5caada114b465017d95933fdc4 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 7 Jun 2024 14:06:52 +0100 Subject: [PATCH 079/103] removed all normalisation of waveforms --- SeisSeeker/processing/detection.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 1cb23dd..5b800af 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -644,8 +644,7 @@ def run_array_proc(self): # Load data: try: - st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour, - norm=self.normalise_data) + st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) except IndexError: # And skip if no data: logger.exception("Skipping hour as no data") @@ -1007,8 +1006,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi event_phase_arr_time = obspy.UTCDateTime(row['t1']) if count == 0: st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour, - norm=self.normalise_data) + event_phase_arr_time.day, hour=event_phase_arr_time.hour) # Find uncertainties: # ------- For vertical -------: @@ -1031,8 +1029,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour, - norm=self.normalise_data) + event_phase_arr_time.day, hour=event_phase_arr_time.hour) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) @@ -1104,8 +1101,7 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Reload data if needed: if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour, - norm=self.normalise_data) + event_phase_arr_time.day, hour=event_phase_arr_time.hour) st_trimmed = st.copy() st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) From 0a15f54023905af526470332287d0904919bbf86 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 7 Jun 2024 14:43:45 +0100 Subject: [PATCH 080/103] fixed bug in _load-data --- SeisSeeker/processing/detection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 5b800af..21869da 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -645,6 +645,7 @@ def run_array_proc(self): # Load data: try: st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) + print(st) except IndexError: # And skip if no data: logger.exception("Skipping hour as no data") @@ -835,8 +836,7 @@ def _load_data(self, year, month, day, hour=None, norm=True): if self.endtime < st[0].stats.endtime: st.trim(endtime=self.endtime) - else: - return st + return st def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" From 738eda78fc6fded6e9ec890310d71ad6e6638e6f Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 7 Jun 2024 14:44:48 +0100 Subject: [PATCH 081/103] removed print statements used for debugging --- SeisSeeker/processing/detection.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 21869da..e45de01 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -388,16 +388,13 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): def _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree=1): """Function to create stacked data st.""" - print(Z_all.shape) Z_inst_phase_all = hilbert(Z_all, axis=1) N_inst_phase_all = hilbert(N_all, axis=1) E_inst_phase_all = hilbert(E_all, axis=1) - print(Z_inst_phase_all.shape) Z_phase_stack = (np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1))) N_phase_stack = (np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1))) E_phase_stack = (np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1))) - print(Z_phase_stack.shape) composite_st = obspy.Stream() # For Z stacked: tr = st[0].copy() @@ -829,8 +826,6 @@ def _load_data(self, year, month, day, hour=None, norm=True): if self.freqmax: st.filter('bandpass', freqmin=self.freqmin, freqmax=self.freqmax) # And trim data, if some lies outside start and end time of beamforming period: - print(self.starttime, st[0].stats.starttime) - print(self.endtime, st[0].stats.endtime) if self.starttime > st[0].stats.starttime: st.trim(starttime=self.starttime) if self.endtime < st[0].stats.endtime: From e298ea9cc4fd5c06c8faadc5702b4c6ec019d0fe Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Mon, 24 Jun 2024 10:17:26 +0100 Subject: [PATCH 082/103] removed print --- SeisSeeker/processing/detection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index e45de01..ea8ec11 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -642,7 +642,6 @@ def run_array_proc(self): # Load data: try: st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) - print(st) except IndexError: # And skip if no data: logger.exception("Skipping hour as no data") From edc4d998532e4a010d0e0961c6d3238cf27c305b Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 20 Aug 2024 15:30:47 +0100 Subject: [PATCH 083/103] removed unneeded comments --- SeisSeeker/processing/detection.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index ea8ec11..0d890ff 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -126,10 +126,7 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n aconj = np.conj(a) Pfreq[ii,ir,itheta]=np.dot(np.dot(aconj,Rxx),a) # Cross-correlation, with two timeshifts applied to push the two stations to the centre point. # (This can also be seen as projecting Rxx onto a new basis.) - - # And remove any data where stations don't exist: - ###np.nan_to_num(Pfreq, copy=False, nan=0.0) # NOT SUPPORTED BY NUMBA SO DO OUTSIDE NUMBA - + # And append output to datastore: Pfreq_all[win_idx,:,:,:] = Pfreq From 2d05f9595b86ed5558e7a4eb2c2ef8158ddb4add Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Sat, 9 Nov 2024 16:01:19 +0000 Subject: [PATCH 084/103] now calculates Rxx if remove_autocorr = False. previously did nothing! --- SeisSeeker/processing/detection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 0d890ff..fd36362 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -73,9 +73,9 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n # (so that don't have to do it for every frequency) tlib = np.zeros((n_stations,n_sl,n_baz), dtype=np.complex128) for ir in range(0,n_sl): - for itheta in range(0,n_baz): - # tlib[:,ix,iy] = xx*ux[ix] + yy*uy[iy] # (distance x slowness = distance / velocity = time) - tlib[:,ir,itheta] = xx*ur[ir]*np.sin((utheta_rad[itheta])) + yy*ur[ir]*np.cos((utheta_rad[itheta])) # (distance x slowness = distance / velocity = time) + for itheta in range(0,n_baz): + # tlib[:,ix,iy] = xx*ux[ix] + yy*uy[iy] # (distance x slowness = distance / velocity = time) + tlib[:,ir,itheta] = xx*ur[ir]*np.sin((utheta_rad[itheta])) + yy*ur[ir]*np.cos((utheta_rad[itheta])) # (distance x slowness = distance / velocity = time) # Since receivers are relative to the array centre, can shift all receivers back to that centre. # Create data stores: @@ -117,7 +117,9 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n Rxx[i1,i2] = np.conj(Pxx_all[curr_f_idx,i1]) * Pxx_all[curr_f_idx,i2] else: Rxx[i1,i2] = 0 - + else: + Rxx[i1,i2] = np.conj(Pxx_all[curr_f_idx,i1]) * Pxx_all[curr_f_idx,i2] + # And loop over phase shifts, calculating cross-correlation power: for ir in range(0,n_sl): for itheta in range(0,n_baz): From cc5f0b619ed98dda144ce4c9f2e9406c4ecdfed2 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 23 May 2025 14:38:45 +0100 Subject: [PATCH 085/103] removed blank line --- SeisSeeker/processing/detection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index fd36362..32928f4 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -119,7 +119,7 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n Rxx[i1,i2] = 0 else: Rxx[i1,i2] = np.conj(Pxx_all[curr_f_idx,i1]) * Pxx_all[curr_f_idx,i2] - + # And loop over phase shifts, calculating cross-correlation power: for ir in range(0,n_sl): for itheta in range(0,n_baz): @@ -127,6 +127,8 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n a = np.exp(-1j*2*np.pi*target_f*timeshifts) # (a is a steering vector, to allign all traces with array centre) aconj = np.conj(a) Pfreq[ii,ir,itheta]=np.dot(np.dot(aconj,Rxx),a) # Cross-correlation, with two timeshifts applied to push the two stations to the centre point. + # np.dot is returning a sum product here making this + # effectively eqn 7 of Ruigrok et al., (2017) # (This can also be seen as projecting Rxx onto a new basis.) # And append output to datastore: From 0665fc977fd37a8017abdf2e3216b81ac9e7f038 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 16 Sep 2025 17:35:26 +0100 Subject: [PATCH 086/103] black linting and removed unused imports --- SeisSeeker/processing/detection.py | 1421 +++++++++++------ SeisSeeker/processing/location.py | 107 +- SeisSeeker/processing/lookup_table_manager.py | 226 +-- .../lookup_table_manager_3D_backup.py | 142 +- 4 files changed, 1182 insertions(+), 714 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 32928f4..f94c78c 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1,35 +1,35 @@ #!/usr/bin/python -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Script Description: # Script to perform earthquake detection using array processing methods. # Created by Tom Hudson, 10th August 2022 -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Import neccessary modules: import pandas as pd import numpy as np -from pathlib import Path, PurePath -import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D -import os, sys +from pathlib import Path +import matplotlib.pyplot as plt +import os import obspy import datetime from scipy.signal import find_peaks, hilbert -from numba import jit, objmode, prange, set_num_threads -import gc +from numba import jit, objmode, prange +import gc import logging -# import multiprocessing as mp -import time -import glob + +import time +import glob import pickle -from SeisSeeker.processing import lookup_table_manager, location +from SeisSeeker.processing import lookup_table_manager, location logger = logging.getLogger(__name__) -#----------------------------------------------- Define main functions ----------------------------------------------- + +# ----------------------------------------------- Define main functions ----------------------------------------------- class CustomError(Exception): pass @@ -38,23 +38,36 @@ def flatten_list(l): return [item for sublist in l for item in sublist] -def xy_to_rtheta(x,y): +def xy_to_rtheta(x, y): """x,y to r,theta, where x,y in East and North directions. Theta is in degrees from N.""" r = np.sqrt(x**2 + y**2) - theta = np.rad2deg(np.arctan2(x,y)) + theta = np.rad2deg(np.arctan2(x, y)) try: - theta[theta<0] = theta[theta<0] + 360 + theta[theta < 0] = theta[theta < 0] + 360 except TypeError: theta = theta + 360 - return r, theta - - -@jit(nopython=True, parallel=True)#, nogil=True) -def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n_baz, fs, target_freqs, xx, yy, - n_stations, n_t_samp, remove_autocorr, - ): - """Function to perform array processing fast due to being designed to + return r, theta + + +@jit(nopython=True, parallel=True) # , nogil=True) +def _fast_freq_domain_array_proc( + data, + min_sl, + max_sl, + n_sl, + min_baz, + max_baz, + n_baz, + fs, + target_freqs, + xx, + yy, + n_stations, + n_t_samp, + remove_autocorr, +): + """Function to perform array processing fast due to being designed to be wrapped using Numba. Function inspired by Bowden et al. (2021). Performs array processing in polar coordinates. Returns: @@ -63,44 +76,52 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n # Define grid of slownesses: # number of pixes in x and y # (Determines number of phase shifts to perform) - ur = np.linspace(min_sl,max_sl,n_sl) + ur = np.linspace(min_sl, max_sl, n_sl) utheta = np.linspace(min_baz, max_baz, n_baz) utheta_rad = np.deg2rad(utheta) - dur=ur[1]-ur[0] - dutheta=utheta[1]-utheta[0] + dur = ur[1] - ur[0] + dutheta = utheta[1] - utheta[0] # Compute time-shifts once: # (so that don't have to do it for every frequency) - tlib = np.zeros((n_stations,n_sl,n_baz), dtype=np.complex128) - for ir in range(0,n_sl): - for itheta in range(0,n_baz): + tlib = np.zeros((n_stations, n_sl, n_baz), dtype=np.complex128) + for ir in range(0, n_sl): + for itheta in range(0, n_baz): # tlib[:,ix,iy] = xx*ux[ix] + yy*uy[iy] # (distance x slowness = distance / velocity = time) - tlib[:,ir,itheta] = xx*ur[ir]*np.sin((utheta_rad[itheta])) + yy*ur[ir]*np.cos((utheta_rad[itheta])) # (distance x slowness = distance / velocity = time) + tlib[:, ir, itheta] = xx * ur[ir] * np.sin((utheta_rad[itheta])) + yy * ur[ + ir + ] * np.cos( + (utheta_rad[itheta]) + ) # (distance x slowness = distance / velocity = time) # Since receivers are relative to the array centre, can shift all receivers back to that centre. # Create data stores: - Pfreq_all = np.zeros((data.shape[0],len(target_freqs),n_sl,n_baz), dtype=np.complex128) # Explicitly create Pxx_all, as otherwise prange won't work correctly. + Pfreq_all = np.zeros( + (data.shape[0], len(target_freqs), n_sl, n_baz), dtype=np.complex128 + ) # Explicitly create Pxx_all, as otherwise prange won't work correctly. # Then loop over windows: for win_idx in prange(data.shape[0]): # Calculate spectra: # Construct data structure: - nfft = (2.0**np.ceil(np.log2(n_t_samp))) + nfft = 2.0 ** np.ceil(np.log2(n_t_samp)) nfft = np.array(nfft, dtype=np.int64) - Pxx_all = np.zeros((np.int64((nfft/2)+1), n_stations), dtype=np.complex128) # Power spectra - dt = 1. / fs - df = 1.0/(2.0*nfft*dt) - xf = np.linspace(0.0, 1.0/(2.0*dt), np.int64((nfft/2)+1)) + Pxx_all = np.zeros( + (np.int64((nfft / 2) + 1), n_stations), dtype=np.complex128 + ) # Power spectra + dt = 1.0 / fs + df = 1.0 / (2.0 * nfft * dt) + xf = np.linspace(0.0, 1.0 / (2.0 * dt), np.int64((nfft / 2) + 1)) # Calculate power spectra for all stations: for sta_idx in range(n_stations): # Calculate spectra for current station: ###Pxx_all[:,sta_idx] = np.fft.rfft(data[win_idx,sta_idx,:], n=nfft) # (Use real fft, as input data is real) # DOESN'T WORK WITH NUMBA! - with objmode(Pxx_curr='complex128[:]'): - Pxx_curr = np.fft.rfft(data[win_idx,sta_idx,:], n=nfft) - Pxx_all[:,sta_idx] = Pxx_curr + with objmode(Pxx_curr="complex128[:]"): + Pxx_curr = np.fft.rfft(data[win_idx, sta_idx, :], n=nfft) + Pxx_all[:, sta_idx] = Pxx_curr # Loop over all freqs, performing phase shifts: - Pfreq=np.zeros((len(target_freqs),n_sl,n_baz),dtype=np.complex128) + Pfreq = np.zeros((len(target_freqs), n_sl, n_baz), dtype=np.complex128) counter_grid = 0 for ii in range(len(target_freqs)): # Find closest current freq.: @@ -108,37 +129,57 @@ def _fast_freq_domain_array_proc(data, min_sl, max_sl, n_sl, min_baz, max_baz, n curr_f_idx = (np.abs(xf - target_f)).argmin() # Construct a matrix of each station-station correlation before any phase shifts - Rxx=np.zeros((n_stations,n_stations),dtype=np.complex128) - for i1 in range(0,n_stations): - for i2 in range(0,n_stations): + Rxx = np.zeros((n_stations, n_stations), dtype=np.complex128) + for i1 in range(0, n_stations): + for i2 in range(0, n_stations): # Remove autocorrelations: if remove_autocorr: if not i1 == i2: - Rxx[i1,i2] = np.conj(Pxx_all[curr_f_idx,i1]) * Pxx_all[curr_f_idx,i2] + Rxx[i1, i2] = ( + np.conj(Pxx_all[curr_f_idx, i1]) + * Pxx_all[curr_f_idx, i2] + ) else: - Rxx[i1,i2] = 0 + Rxx[i1, i2] = 0 else: - Rxx[i1,i2] = np.conj(Pxx_all[curr_f_idx,i1]) * Pxx_all[curr_f_idx,i2] + Rxx[i1, i2] = ( + np.conj(Pxx_all[curr_f_idx, i1]) * Pxx_all[curr_f_idx, i2] + ) # And loop over phase shifts, calculating cross-correlation power: - for ir in range(0,n_sl): - for itheta in range(0,n_baz): - timeshifts = tlib[:,ir,itheta] # Calculate the "steering vector" (a vector in frequency space, based on phase-shift) - a = np.exp(-1j*2*np.pi*target_f*timeshifts) # (a is a steering vector, to allign all traces with array centre) + for ir in range(0, n_sl): + for itheta in range(0, n_baz): + timeshifts = tlib[ + :, ir, itheta + ] # Calculate the "steering vector" (a vector in frequency space, based on phase-shift) + a = np.exp( + -1j * 2 * np.pi * target_f * timeshifts + ) # (a is a steering vector, to allign all traces with array centre) aconj = np.conj(a) - Pfreq[ii,ir,itheta]=np.dot(np.dot(aconj,Rxx),a) # Cross-correlation, with two timeshifts applied to push the two stations to the centre point. - # np.dot is returning a sum product here making this + Pfreq[ii, ir, itheta] = np.dot( + np.dot(aconj, Rxx), a + ) # Cross-correlation, with two timeshifts applied to push the two stations to the centre point. + # np.dot is returning a sum product here making this # effectively eqn 7 of Ruigrok et al., (2017) # (This can also be seen as projecting Rxx onto a new basis.) - + # And append output to datastore: - Pfreq_all[win_idx,:,:,:] = Pfreq + Pfreq_all[win_idx, :, :, :] = Pfreq - return Pfreq_all + return Pfreq_all # @jit(nopython=True, parallel=True)#, nogil=True) -def _phase_associator_core_worker(peaks_Z, peaks_hor, bazis_Z, bazis_hor, bazi_tol, t_Z_secs_after_start, t_hor_secs_after_start, max_phase_sep_s): +def _phase_associator_core_worker( + peaks_Z, + peaks_hor, + bazis_Z, + bazis_hor, + bazi_tol, + t_Z_secs_after_start, + t_hor_secs_after_start, + max_phase_sep_s, +): """Function to do the heavy lifting of the phase association.""" # Specify data stores: Z_hor_phase_pair_idxs = [] @@ -146,19 +187,22 @@ def _phase_associator_core_worker(peaks_Z, peaks_hor, bazis_Z, bazis_hor, bazi_t # Loop over phases, seeing if they meet phase association criteria: for i in range(len(peaks_Z)): if i % 1000 == 0: - print(i,"/",len(peaks_Z)) + print(i, "/", len(peaks_Z)) curr_peak_Z_idx = peaks_Z[i] for j in range(len(peaks_hor)): curr_peak_hor_idx = peaks_hor[j] # i. Check if phase arrivals are within specified time limits: - curr_t_phase_diff = t_hor_secs_after_start[curr_peak_hor_idx] - t_Z_secs_after_start[curr_peak_Z_idx] + curr_t_phase_diff = ( + t_hor_secs_after_start[curr_peak_hor_idx] + - t_Z_secs_after_start[curr_peak_Z_idx] + ) if curr_t_phase_diff > 0: if curr_t_phase_diff <= max_phase_sep_s: # ii. Check if bazis for Z and horizontals current pick match: - if np.abs( bazis_Z[i] - bazis_hor[j] ) < bazi_tol: + if np.abs(bazis_Z[i] - bazis_hor[j]) < bazi_tol: match = True # And deal with if they are close to North: - elif np.abs( bazis_Z[i] - bazis_hor[j] ) > (360. - bazi_tol): + elif np.abs(bazis_Z[i] - bazis_hor[j]) > (360.0 - bazi_tol): match = True else: match = False @@ -166,61 +210,95 @@ def _phase_associator_core_worker(peaks_Z, peaks_hor, bazis_Z, bazis_hor, bazi_t # And associate phases and create event data if a match is found: if match: # Append pair idxs to data store: - Z_hor_phase_pair_idxs.append([curr_peak_Z_idx, curr_peak_hor_idx]) - + Z_hor_phase_pair_idxs.append( + [curr_peak_Z_idx, curr_peak_hor_idx] + ) + return Z_hor_phase_pair_idxs -def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_tol, filt_phase_assoc_by_max_power, max_phase_sep_s, min_event_sep_s, verbosity=0): + +def _phase_associator( + t_series_df_Z, + t_series_df_hor, + peaks_Z, + peaks_hor, + bazi_tol, + filt_phase_assoc_by_max_power, + max_phase_sep_s, + min_event_sep_s, + verbosity=0, +): """ Function to perform phase association for numba implementation. """ # Setup events datastores: list_of_curr_event_dfs = [] # Find back-azimuths associated with phase picks: - bazis_Z = t_series_df_Z['back_azi'].values[peaks_Z] - bazis_hor = t_series_df_hor['back_azi'].values[peaks_hor] + bazis_Z = t_series_df_Z["back_azi"].values[peaks_Z] + bazis_hor = t_series_df_hor["back_azi"].values[peaks_hor] - #------------------------------------------------------------------- + # ------------------------------------------------------------------- # Perform core phae association: # Prep. data for numba format: if verbosity > 1: logger.info("Pre-processing time-series") t_Z_secs_after_start = [] for index, row in t_series_df_Z.iterrows(): - t_Z_secs_after_start.append(obspy.UTCDateTime(row['t']) - obspy.UTCDateTime(t_series_df_Z['t'][0])) + t_Z_secs_after_start.append( + obspy.UTCDateTime(row["t"]) - obspy.UTCDateTime(t_series_df_Z["t"][0]) + ) t_hor_secs_after_start = [] for index, row in t_series_df_hor.iterrows(): - t_hor_secs_after_start.append(obspy.UTCDateTime(row['t']) - obspy.UTCDateTime(t_series_df_hor['t'][0])) + t_hor_secs_after_start.append( + obspy.UTCDateTime(row["t"]) - obspy.UTCDateTime(t_series_df_hor["t"][0]) + ) # Run function: if verbosity > 1: logger.info("Performing phase association") - Z_hor_phase_pair_idxs = _phase_associator_core_worker(peaks_Z, peaks_hor, bazis_Z, bazis_hor, bazi_tol, t_Z_secs_after_start, t_hor_secs_after_start, max_phase_sep_s) + Z_hor_phase_pair_idxs = _phase_associator_core_worker( + peaks_Z, + peaks_hor, + bazis_Z, + bazis_hor, + bazi_tol, + t_Z_secs_after_start, + t_hor_secs_after_start, + max_phase_sep_s, + ) # Organise outputs into useful form: if verbosity > 1: logger.info("Writing events") - curr_events = {'t1':[],'t2':[], 'pow1': [], 'pow2':[], 'slow1':[], - 'slow2':[], 'bazi1':[], 'bazi2':[]} - + curr_events = { + "t1": [], + "t2": [], + "pow1": [], + "pow2": [], + "slow1": [], + "slow2": [], + "bazi1": [], + "bazi2": [], + } + for event_idx in range(len(Z_hor_phase_pair_idxs)): curr_peak_Z_idx = Z_hor_phase_pair_idxs[event_idx][0] curr_peak_hor_idx = Z_hor_phase_pair_idxs[event_idx][1] - curr_events['t1'].append(t_series_df_Z['t'][curr_peak_Z_idx]) - curr_events['t2'].append(t_series_df_hor['t'][curr_peak_hor_idx]) - curr_events['pow1'].append(t_series_df_Z['power'][curr_peak_Z_idx]) - curr_events['pow2'].append(t_series_df_hor['power'][curr_peak_hor_idx]) - curr_events['slow1'].append(t_series_df_Z['slowness'][curr_peak_Z_idx]) - curr_events['slow2'].append(t_series_df_hor['slowness'][curr_peak_hor_idx]) - curr_events['bazi1'].append(t_series_df_Z['back_azi'][curr_peak_Z_idx]) - curr_events['bazi2'].append(t_series_df_hor['back_azi'][curr_peak_hor_idx]) + curr_events["t1"].append(t_series_df_Z["t"][curr_peak_Z_idx]) + curr_events["t2"].append(t_series_df_hor["t"][curr_peak_hor_idx]) + curr_events["pow1"].append(t_series_df_Z["power"][curr_peak_Z_idx]) + curr_events["pow2"].append(t_series_df_hor["power"][curr_peak_hor_idx]) + curr_events["slow1"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) + curr_events["slow2"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) + curr_events["bazi1"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) + curr_events["bazi2"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) events_df = pd.DataFrame(curr_events) # And tidy: del t_Z_secs_after_start, t_hor_secs_after_start, Z_hor_phase_pair_idxs gc.collect() - #------------------------------------------------------------------- + # ------------------------------------------------------------------- - # And filter events to only output events with max. power within the max. phase window, + # And filter events to only output events with max. power within the max. phase window, # if speficifed by user: if filt_phase_assoc_by_max_power: # Only process if found some events: @@ -232,14 +310,17 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # And loop over events, selecting only max. power events: tmp_count = 0 for index, row in events_df.iterrows(): - tmp_count+=1 + tmp_count += 1 if tmp_count == 1: tmp_lst = [] tmp_lst.append(row) else: # Append event if phase within minimum event separation: - if obspy.UTCDateTime(row['t1']) - obspy.UTCDateTime(tmp_lst[0].t1) < min_event_sep_s: + if ( + obspy.UTCDateTime(row["t1"]) - obspy.UTCDateTime(tmp_lst[0].t1) + < min_event_sep_s + ): # Append event to compare: tmp_lst.append(row) else: @@ -261,13 +342,13 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t # And remove duplicate S pick associations: # (using same max. power method) # Append summed powers, for sorting: - sum_pows = filt_events_df['pow1'].values + filt_events_df['pow2'].values - filt_events_df['sum_pows'] = sum_pows + sum_pows = filt_events_df["pow1"].values + filt_events_df["pow2"].values + filt_events_df["sum_pows"] = sum_pows # Remove t2 duplicates, keep highest summed power: - filt_events_df.sort_values('sum_pows', inplace=True) - filt_events_df.drop_duplicates(subset='t2', keep='last', inplace=True) + filt_events_df.sort_values("sum_pows", inplace=True) + filt_events_df.drop_duplicates(subset="t2", keep="last", inplace=True) # And remove sum_pows column: - filt_events_df.drop(columns=['sum_pows'], inplace=True) + filt_events_df.drop(columns=["sum_pows"], inplace=True) # And output df: events_df = filt_events_df.copy() @@ -276,6 +357,7 @@ def _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, bazi_t return events_df + def _find_max_power_event(events): """ Find the maximum power event from a list of events @@ -293,10 +375,33 @@ def _find_max_power_event(events): max_power_event = events[max_power_idx] return max_power_event -def _submit_parallel_fast_freq_domain_array_proc(procnum, return_dict_Pfreq_all, data_curr_run, max_sl, fs, target_freqs, xx, yy, n_stations, n_t_samp, remove_autocorr): + +def _submit_parallel_fast_freq_domain_array_proc( + procnum, + return_dict_Pfreq_all, + data_curr_run, + max_sl, + fs, + target_freqs, + xx, + yy, + n_stations, + n_t_samp, + remove_autocorr, +): """Function to submit parallel runs of _fast_freq_domain_array_proc() function.""" # Run function: - Pfreq_all_curr_run = _fast_freq_domain_array_proc(data_curr_run, max_sl, fs, target_freqs, xx, yy, n_stations, n_t_samp, remove_autocorr) + Pfreq_all_curr_run = _fast_freq_domain_array_proc( + data_curr_run, + max_sl, + fs, + target_freqs, + xx, + yy, + n_stations, + n_t_samp, + remove_autocorr, + ) # And return data return_dict_Pfreq_all[procnum] = Pfreq_all_curr_run @@ -304,11 +409,11 @@ def _submit_parallel_fast_freq_domain_array_proc(procnum, return_dict_Pfreq_all, def _calc_time_shift_from_array_cent(slow, bazi, x_rec, y_rec): """Calculates time shift of signal at receiver from array centre for stacking data. Note: All distances and velocities use km unless otherwise specified. - + Parameters ---------- slow : float - Slowness of arrival, in s/km. + Slowness of arrival, in s/km. bazi : float Back azimuth of arrival in degrees from . x_rec : @@ -319,7 +424,9 @@ def _calc_time_shift_from_array_cent(slow, bazi, x_rec, y_rec): # Calculate time-shift for receiver: # (in polar coord system, for consistency) bazi_rad = np.deg2rad(bazi) - time_shift_curr = x_rec*slow*np.sin(bazi_rad) + y_rec*slow*np.cos(bazi_rad) # (distance x slowness = distance / velocity = time) + time_shift_curr = x_rec * slow * np.sin(bazi_rad) + y_rec * slow * np.cos( + bazi_rad + ) # (distance x slowness = distance / velocity = time) return time_shift_curr @@ -330,55 +437,55 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): # For Z stacked: tr = st[0].copy() tr.stats.station = "STACK" - tr.stats.channel = st[0].stats.channel[0:2]+"Z" + tr.stats.channel = st[0].stats.channel[0:2] + "Z" tr.data = np.sum(Z_all, axis=1) composite_st.append(tr) # For Z mean: tr = st[0].copy() tr.stats.station = "MEAN" - tr.stats.channel = st[0].stats.channel[0:2]+"Z" + tr.stats.channel = st[0].stats.channel[0:2] + "Z" tr.data = np.mean(Z_all, axis=1) composite_st.append(tr) # For Z stdev: tr = st[0].copy() tr.stats.station = "STDEV" - tr.stats.channel = st[0].stats.channel[0:2]+"Z" + tr.stats.channel = st[0].stats.channel[0:2] + "Z" tr.data = np.std(Z_all, axis=1) composite_st.append(tr) # For N stacked: tr = st[0].copy() tr.stats.station = "STACK" - tr.stats.channel = st[0].stats.channel[0:2]+"N" + tr.stats.channel = st[0].stats.channel[0:2] + "N" tr.data = np.sum(N_all, axis=1) composite_st.append(tr) # For N mean: tr = st[0].copy() tr.stats.station = "MEAN" - tr.stats.channel = st[0].stats.channel[0:2]+"N" + tr.stats.channel = st[0].stats.channel[0:2] + "N" tr.data = np.mean(N_all, axis=1) composite_st.append(tr) # For N stdev: tr = st[0].copy() tr.stats.station = "STDEV" - tr.stats.channel = st[0].stats.channel[0:2]+"N" + tr.stats.channel = st[0].stats.channel[0:2] + "N" tr.data = np.std(N_all, axis=1) composite_st.append(tr) # For E stacked: tr = st[0].copy() tr.stats.station = "STACK" - tr.stats.channel = st[0].stats.channel[0:2]+"E" + tr.stats.channel = st[0].stats.channel[0:2] + "E" tr.data = np.sum(E_all, axis=1) composite_st.append(tr) # For E mean: tr = st[0].copy() tr.stats.station = "MEAN" - tr.stats.channel = st[0].stats.channel[0:2]+"E" + tr.stats.channel = st[0].stats.channel[0:2] + "E" tr.data = np.mean(E_all, axis=1) composite_st.append(tr) # For E stdev: tr = st[0].copy() tr.stats.station = "STDEV" - tr.stats.channel = st[0].stats.channel[0:2]+"E" + tr.stats.channel = st[0].stats.channel[0:2] + "E" tr.data = np.std(E_all, axis=1) composite_st.append(tr) del tr @@ -386,6 +493,7 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): return composite_st + def _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree=1): """Function to create stacked data st.""" @@ -393,34 +501,35 @@ def _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree=1): N_inst_phase_all = hilbert(N_all, axis=1) E_inst_phase_all = hilbert(E_all, axis=1) - Z_phase_stack = (np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1))) - N_phase_stack = (np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1))) - E_phase_stack = (np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1))) + Z_phase_stack = np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1)) + N_phase_stack = np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1)) + E_phase_stack = np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1)) composite_st = obspy.Stream() # For Z stacked: tr = st[0].copy() tr.stats.station = "PW-STACK" - tr.stats.channel = st[0].stats.channel[0:2]+"Z" - tr.data = np.mean(Z_all, axis=1)*(Z_phase_stack**degree) + tr.stats.channel = st[0].stats.channel[0:2] + "Z" + tr.data = np.mean(Z_all, axis=1) * (Z_phase_stack**degree) print(tr.data.shape) composite_st.append(tr) # For N stacked: tr = st[0].copy() tr.stats.station = "PW-STACK" - tr.stats.channel = st[0].stats.channel[0:2]+"N" - tr.data = np.mean(N_all, axis=1)*(N_phase_stack**degree) + tr.stats.channel = st[0].stats.channel[0:2] + "N" + tr.data = np.mean(N_all, axis=1) * (N_phase_stack**degree) composite_st.append(tr) # For E stacked: tr = st[0].copy() tr.stats.station = "PW-STACK" - tr.stats.channel = st[0].stats.channel[0:2]+"E" - tr.data = np.mean(E_all, axis=1)*(E_phase_stack**degree) + tr.stats.channel = st[0].stats.channel[0:2] + "E" + tr.data = np.mean(E_all, axis=1) * (E_phase_stack**degree) composite_st.append(tr) del tr gc.collect() return composite_st + class setup_detection: """ Class to create detection object, for running array detection algorithm. @@ -435,7 +544,7 @@ class setup_detection: Path to directory to save outputs to. stations_fname : str - Path to csv file containing station/receiver locations. Headers need + Path to csv file containing station/receiver locations. Headers need to be of format: Latitude Longitude Elevation Name. starttime : obspy UTCDateTime object @@ -446,8 +555,8 @@ class setup_detection: channels_to_use : list of strs (optional, default = ["??Z"]) List of channels to use for the processing (e.g. HHZ, ??Z or similar). - Note: Currently must be in form: ["??Z"] or ["??Z", "??N", "??E"] - or ["??Z", "??1", "??2"]. + Note: Currently must be in form: ["??Z"] or ["??Z", "??N", "??E"] + or ["??Z", "??1", "??2"]. Attributes ---------- @@ -462,9 +571,9 @@ class setup_detection: If specified, upper frequency of bandpass filter, in Hz. Default is None. num_freqs : int - Number of discrete frequencies to use between and - in analysis. Default is 100. Note: Reducing this value increases - efficiency linearly, but costs in terms of absolute power. However, + Number of discrete frequencies to use between and + in analysis. Default is 100. Note: Reducing this value increases + efficiency linearly, but costs in terms of absolute power. However, SNR of power time-series remains approximately constant (to a point). Can affect slowness and bazi results, especially within noise, though. @@ -476,21 +585,21 @@ class setup_detection: Default value is 0.1 s. win_step_inc_s : float - The step increment for each window step. Units are seconds. Note: set this value - equal to for no overlap of windows. overlap between windows is given - by win_len_s - win_step_inc_s. For example, if win_step_inc_s = 3 x - then overlap will be 2 x , etc. Greater overlap gives higher frequency + The step increment for each window step. Units are seconds. Note: set this value + equal to for no overlap of windows. overlap between windows is given + by win_len_s - win_step_inc_s. For example, if win_step_inc_s = 3 x + then overlap will be 2 x , etc. Greater overlap gives higher frequency resolution, but at computational cost. Default value is 0.1 s. - + remove_autocorr : bool If True, then will remove autocorrelations. Default is True. norm_pre_stacking : bool - If True, normallises data before stacking. Similar to performing spectral + If True, normallises data before stacking. Similar to performing spectral whitening. Default is False mad_window_length_s : float - Length of time-window, in seconds, to calculate background Median Absolute + Length of time-window, in seconds, to calculate background Median Absolute Deviation (MAD) for triggering events. Default is 3600 s. mad_multiplier : int @@ -500,26 +609,35 @@ class setup_detection: min_event_sep_s : float Minimum separation between event detections, in seconds. Default = 1 s. - bazi_tol : float + bazi_tol : float The back-azimuth tolerance to associate incoming phases with the same event. - Various phases have to fulfil the criteria of having the same back-azimuth + Various phases have to fulfil the criteria of having the same back-azimuth for all phase arrivals, +/- . Units are degrees. Defaul is 20 degrees. max_phase_sep_s : float - The maximum time separation between individual phases. Units are seconds. Default + The maximum time separation between individual phases. Units are seconds. Default is 2.5 s. filt_phase_assoc_by_max_power : bool - If True, filters event phase association within a given window by max. power. I.e. - if True, will only pick highest amplitude phase arrivals on both vertical and + If True, filters event phase association within a given window by max. power. I.e. + if True, will only pick highest amplitude phase arrivals on both vertical and horizontal components, within the time window . Methods ------- """ - - def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, preload_fname=None, channels_to_use=["??Z"]): + + def __init__( + self, + archivedir, + outdir, + stations_fname, + starttime, + endtime, + preload_fname=None, + channels_to_use=["??Z"], + ): """Initiate the class object. Parameters @@ -532,7 +650,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo Path to directory to save outputs to. stations_fname : str - Path to csv file containing station/receiver locations. Headers need + Path to csv file containing station/receiver locations. Headers need to be of format: Latitude Longitude Elevation Name. starttime : obspy UTCDateTime object @@ -542,7 +660,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo End time of data window to process for. preload_fname : str - Path to previously created detection class object. Optional. Default is + Path to previously created detection class object. Optional. Default is None, which means it doesn't load an existing file. channels_to_use : list of strs (optional, default = ["??Z"]) @@ -577,7 +695,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo self.max_baz = 360 self.n_baz = 181 self.win_len_s = 0.1 - self.win_step_inc_s = 0.1 # (Note: Default is to step with no overlap) + self.win_step_inc_s = 0.1 # (Note: Default is to step with no overlap) self.remove_autocorr = True self.norm_pre_stacking = False # self.nproc = 1 @@ -585,11 +703,11 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo # For detection: self.mad_window_length_s = 3600 self.mad_multiplier = 8 - self.min_event_sep_s = 1. - self.bazi_tol = 20. + self.min_event_sep_s = 1.0 + self.bazi_tol = 20.0 self.max_phase_sep_s = 2.5 self.filt_phase_assoc_by_max_power = True - self.calc_uncertainties = False + self.calc_uncertainties = False # For location: self.receiver_vp = None self.receiver_vs = None @@ -601,7 +719,7 @@ def __init__(self, archivedir, outdir, stations_fname, starttime, endtime, prelo def run_array_proc(self): """Function to run core array processing. - Performed in frequency domain. Involves applying phase (equiv. to time) shift + Performed in frequency domain. Involves applying phase (equiv. to time) shift for each frequency, over a range of specified slownesses. Function inspured by work of D. Bowden (see Bowden et al. (2020)).""" @@ -609,96 +727,137 @@ def run_array_proc(self): dt_start = self.starttime.date dt_end = self.endtime.date if (dt_end - dt_start).days == 0: - #round up to one day at minimum + # round up to one day at minimum ndays = 1 else: ndays = (dt_end - dt_start).days - query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0,ndays)] + query_dates = [dt_start + datetime.timedelta(days=d) for d in range(0, ndays)] for date in query_dates: # Loop over dates within start/end range: # Loop over channels: for self.channel_curr in self.channels_to_use: - logger.info("="*60) - logger.info(f"Processing data for day {date}, channel {self.channel_curr}") + logger.info("=" * 60) + logger.info( + f"Processing data for day {date}, channel {self.channel_curr}" + ) # And process for individual hours: # (to reduce memory usage) for hour in range(24): # Loop over every hour in every day.. - # Make outfile - outfile = f'detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv' + # Make outfile + outfile = f"detection_t_series_{date.year:02d}{date.month:02d}{date.day:02d}_{hour:02d}00_ch{self.channel_curr[-1]}.csv" if ((self.outdir / outfile).is_file()) & (self.skip_existing): - logger.warning(f'{outfile} exists in {self.outdir}') - logger.warning('Move to next hour') - self.out_fnames_array_proc.append(f'{self.outdir}/{outfile}') + logger.warning(f"{outfile} exists in {self.outdir}") + logger.warning("Move to next hour") + self.out_fnames_array_proc.append(f"{self.outdir}/{outfile}") continue - if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour) + 3600: + if ( + self.starttime + >= obspy.UTCDateTime( + year=date.year, month=date.month, day=date.day, hour=hour + ) + + 3600 + ): continue - if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour): + if self.endtime <= obspy.UTCDateTime( + year=date.year, month=date.month, day=date.day, hour=hour + ): continue # Create datastores: - data_store = {'t': [], 'power': [], 'slowness': [], 'back_azi': []} + data_store = {"t": [], "power": [], "slowness": [], "back_azi": []} # Load data: try: - st = self._load_data(year=date.year, month=date.month, day=date.day, hour=hour) + st = self._load_data( + year=date.year, month=date.month, day=date.day, hour=hour + ) except IndexError: # And skip if no data: logger.exception("Skipping hour as no data") - del st + del st gc.collect() continue - + # And loop over minutes (to save on memory issues): for minute in range(60): # Check whether specified window is greater than a minute in duration: if self.endtime - self.starttime > 60: # Check time within specified run window: - if self.starttime >= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute) + 60: + if ( + self.starttime + >= obspy.UTCDateTime( + year=date.year, + month=date.month, + day=date.day, + hour=hour, + minute=minute, + ) + + 60 + ): continue - if self.endtime <= obspy.UTCDateTime(year=date.year, month=date.month, day=date.day, hour=hour, minute=minute): + if self.endtime <= obspy.UTCDateTime( + year=date.year, + month=date.month, + day=date.day, + hour=hour, + minute=minute, + ): continue elif self.starttime.minute != minute: continue - + # Trim data: st_trimmed = st.copy() if self.win_len_s > self.win_step_inc_s: self.win_pad_s = self.win_len_s else: - self.win_pad_s = 0. + self.win_pad_s = 0.0 if self.endtime - self.starttime > 60: - st_trimmed.trim(starttime=obspy.UTCDateTime(year=date.year, - month=date.month, - day=date.day, - hour=hour, - minute=minute), - endtime=obspy.UTCDateTime(year=date.year, - month=date.month, - day=date.day, - hour=hour, - minute=minute)+60+self.win_pad_s) + st_trimmed.trim( + starttime=obspy.UTCDateTime( + year=date.year, + month=date.month, + day=date.day, + hour=hour, + minute=minute, + ), + endtime=obspy.UTCDateTime( + year=date.year, + month=date.month, + day=date.day, + hour=hour, + minute=minute, + ) + + 60 + + self.win_pad_s, + ) else: - st_trimmed.trim(starttime=self.starttime, endtime=self.endtime+self.win_pad_s) + st_trimmed.trim( + starttime=self.starttime, + endtime=self.endtime + self.win_pad_s, + ) time_this_minute_st = st_trimmed[0].stats.starttime # Run array processing: # (to get power in slowness space) Psum_all = self._beamforming(st_trimmed) - del st_trimmed + del st_trimmed gc.collect() # Calculate time-series outputs (for detection) from data: - t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) + t_series, powers, slownesses, back_azis = ( + self._find_time_series(Psum_all) + ) # And append to data out: t_series_out = [] for t_serie in t_series: - t_series_out.append( str(time_this_minute_st + t_serie) ) + t_series_out.append(str(time_this_minute_st + t_serie)) - data_store['t'].extend(t_series_out) - data_store['power'].extend(powers) - data_store['slowness'].extend(slownesses) - data_store['back_azi'].extend(back_azis) + data_store["t"].extend(t_series_out) + data_store["power"].extend(powers) + data_store["slowness"].extend(slownesses) + data_store["back_azi"].extend(back_azis) # And clear memory: del Psum_all, t_series, powers, slownesses, back_azis @@ -706,45 +865,62 @@ def run_array_proc(self): # And save data out: out_fname = os.path.join(self.outdir, outfile) - #make DataFrame "just-in-time" as it is more efficient this way + # make DataFrame "just-in-time" as it is more efficient this way store_df = pd.DataFrame(data_store) store_df.to_csv(out_fname, index=False) # And append fname to history: self.out_fnames_array_proc.append(out_fname) - + return None def _setup_array_receiver_coords(self): - """Function to setup station receiver coords in correct format for + """Function to setup station receiver coords in correct format for array processing.""" - ref_lat = np.mean(self.stations_df['Latitude']) - ref_lon = np.mean(self.stations_df['Longitude']) + ref_lat = np.mean(self.stations_df["Latitude"]) + ref_lon = np.mean(self.stations_df["Longitude"]) # Calculate receiver locations in km grid format: xs = [] ys = [] for index, row in self.stations_df.iterrows(): - lon2 = row['Longitude'] - lat2 = row['Latitude'] + lon2 = row["Longitude"] + lat2 = row["Latitude"] # Calc. inter-station distance: - r, a, b = obspy.geodetics.base.gps2dist_azimuth(ref_lat, ref_lon, lat2, lon2) + r, a, b = obspy.geodetics.base.gps2dist_azimuth( + ref_lat, ref_lon, lat2, lon2 + ) xs.append(r * np.sin(np.deg2rad(a)) / 1000) ys.append(r * np.cos(np.deg2rad(a)) / 1000) - self.stations_df['x (km)'] = xs - self.stations_df['y (km)'] = ys - self.stations_df['z (km)'] = self.stations_df['Elevation'].values / 1000. - self.stations_df['ref_lat'] = np.ones(len(xs)) * ref_lat - self.stations_df['ref_lon'] = np.ones(len(xs)) * ref_lon + self.stations_df["x (km)"] = xs + self.stations_df["y (km)"] = ys + self.stations_df["z (km)"] = self.stations_df["Elevation"].values / 1000.0 + self.stations_df["ref_lat"] = np.ones(len(xs)) * ref_lat + self.stations_df["ref_lon"] = np.ones(len(xs)) * ref_lon # And calculate receiver locations in terms of array centre: - array_centre = np.array([np.mean(self.stations_df['x (km)']), np.mean(self.stations_df['y (km)']), np.mean(self.stations_df['z (km)'])]) - self.stations_df['x_array_coords_km'] = self.stations_df['x (km)'].values - array_centre[0] - self.stations_df['y_array_coords_km'] = self.stations_df['y (km)'].values - array_centre[1] - self.stations_df['z_array_coords_km'] = self.stations_df['z (km)'].values - array_centre[2] + array_centre = np.array( + [ + np.mean(self.stations_df["x (km)"]), + np.mean(self.stations_df["y (km)"]), + np.mean(self.stations_df["z (km)"]), + ] + ) + self.stations_df["x_array_coords_km"] = ( + self.stations_df["x (km)"].values - array_centre[0] + ) + self.stations_df["y_array_coords_km"] = ( + self.stations_df["y (km)"].values - array_centre[1] + ) + self.stations_df["z_array_coords_km"] = ( + self.stations_df["z (km)"].values - array_centre[2] + ) # And in polar coords from N: - self.stations_df['r_array_coords_km'], self.stations_df['theta_array_coords_deg'] = xy_to_rtheta(self.stations_df['x_array_coords_km'], - self.stations_df['y_array_coords_km']) - + ( + self.stations_df["r_array_coords_km"], + self.stations_df["theta_array_coords_deg"], + ) = xy_to_rtheta( + self.stations_df["x_array_coords_km"], self.stations_df["y_array_coords_km"] + ) def find_min_max_array_sensitivity(self, vel_assumed=3.0): """Function to find array min and max sensitivities. @@ -757,24 +933,31 @@ def find_min_max_array_sensitivity(self, vel_assumed=3.0): inter_station_dists = [] # Loop over first set of stations: for index, row in self.stations_df.iterrows(): - lon1 = row['Longitude'] - lat1 = row['Latitude'] + lon1 = row["Longitude"] + lat1 = row["Latitude"] # Loop over stations again: for index, row in self.stations_df.iterrows(): - lon2 = row['Longitude'] - lat2 = row['Latitude'] + lon2 = row["Longitude"] + lat2 = row["Latitude"] # Calc. inter-station distance: r, a, b = obspy.geodetics.base.gps2dist_azimuth(lat1, lon1, lat2, lon2) inter_station_dists.append(np.abs(r) / 1000) inter_station_dists = np.array(inter_station_dists) inter_station_dists = inter_station_dists[inter_station_dists != 0] - print("="*60) + print("=" * 60) print("Min. inter-station distance:", np.min(inter_station_dists), "km") - print("Therefore, optimal sensitive higher freq.:", vel_assumed / np.min(inter_station_dists), "Hz") + print( + "Therefore, optimal sensitive higher freq.:", + vel_assumed / np.min(inter_station_dists), + "Hz", + ) print("Max. inter-station distance:", np.max(inter_station_dists), "km") - print("Therefore, optimal sensitive lower freq.:", vel_assumed / np.max(inter_station_dists), "Hz") - print("="*60) - + print( + "Therefore, optimal sensitive lower freq.:", + vel_assumed / np.max(inter_station_dists), + "Hz", + ) + print("=" * 60) def _load_data(self, year, month, day, hour=None, norm=True): """ @@ -786,45 +969,51 @@ def _load_data(self, year, month, day, hour=None, norm=True): year : int year to load data for (yyyy) - month : int - month to load data for. Leading 0's will be added. - day : int + month : int + month to load data for. Leading 0's will be added. + day : int day to load data for. Leading 0's will be added. hour : int, Optional hour to load data for. Leading 0's will be added. - + Returns: ---------- data : obspy.Stream Waveform data for requested date and time. """ # Load in data: - mseed_dir = Path(self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2)) + mseed_dir = Path( + self.archivedir, str(year), str(month).zfill(2), str(day).zfill(2) + ) # print(mseed_dir) st = obspy.Stream() for index, row in self.stations_df.iterrows(): # [J Asplet - think about replacing station DataFrame with StatonXML object] - station = row['Name'] + station = row["Name"] for channel in self.channels_to_use: if hour is None: - timestamp = f'{year:02d}{month:02d}{day:02d}T*' + timestamp = f"{year:02d}{month:02d}{day:02d}T*" else: - timestamp = f'{year:02d}{month:02d}{day:02d}T{hour:02d}0000' + timestamp = f"{year:02d}{month:02d}{day:02d}T{hour:02d}0000" try: - st_tmp = obspy.read(f'{mseed_dir}/{timestamp}_{station}_{channel}.mseed') + st_tmp = obspy.read( + f"{mseed_dir}/{timestamp}_{station}_{channel}.mseed" + ) for tr in st_tmp: st.append(tr) except: - logger.exception(f"No data for {station}, channel = {channel}, timestamp {timestamp}. Skipping this data.") + logger.exception( + f"No data for {station}, channel = {channel}, timestamp {timestamp}. Skipping this data." + ) continue # Merge data: - st.detrend('demean') - st.detrend('linear') - st.merge(method=1, fill_value=0.) + st.detrend("demean") + st.detrend("linear") + st.merge(method=1, fill_value=0.0) # And apply filter: if self.freqmin: if self.freqmax: - st.filter('bandpass', freqmin=self.freqmin, freqmax=self.freqmax) + st.filter("bandpass", freqmin=self.freqmin, freqmax=self.freqmax) # And trim data, if some lies outside start and end time of beamforming period: if self.starttime > st[0].stats.starttime: st.trim(starttime=self.starttime) @@ -835,11 +1024,14 @@ def _load_data(self, year, month, day, hour=None, norm=True): def _convert_st_to_np_data(self, st): """Function to convert data to numpy format for processing.""" - self.n_win = int(((st[0].stats.endtime - self.win_pad_s) - st[0].stats.starttime) / self.win_step_inc_s) # (Note: endtime - self.win_pad_s as pass extra padding via trimmed st) + self.n_win = int( + ((st[0].stats.endtime - self.win_pad_s) - st[0].stats.starttime) + / self.win_step_inc_s + ) # (Note: endtime - self.win_pad_s as pass extra padding via trimmed st) self.fs = st[0].stats.sampling_rate - self.n_t_samp = int(self.win_len_s * self.fs) # num samples in time - station_labels = self.stations_df['Name'].values - self.n_stations = len(station_labels) # num stations + self.n_t_samp = int(self.win_len_s * self.fs) # num samples in time + station_labels = self.stations_df["Name"].values + self.n_stations = len(station_labels) # num stations data = np.zeros((self.n_win, self.n_stations, self.n_t_samp)) for i in range(self.n_win): for j in range(self.n_stations): @@ -847,31 +1039,41 @@ def _convert_st_to_np_data(self, st): win_start_idx = i * int(self.win_step_inc_s * self.fs) win_end_idx = (i * int(self.win_step_inc_s * self.fs)) + self.n_t_samp try: - if win_end_idx < len(st.select(station=station, channel=self.channel_curr)[0].data): - data[i,j,:] = st.select(station=station, channel=self.channel_curr)[0].data[win_start_idx:win_end_idx] + if win_end_idx < len( + st.select(station=station, channel=self.channel_curr)[0].data + ): + data[i, j, :] = st.select( + station=station, channel=self.channel_curr + )[0].data[win_start_idx:win_end_idx] else: # Zero pad data (as insufficient data passed for final window) and print warning: - data[i,j,:] = 0. - logger.warning(f"Warning: Zero-padding as not enough data to fill window overlap ( for win_len_s = {self.win_len_s}, and win_step_inc_s = {self.win_step_inc_s})") + data[i, j, :] = 0.0 + logger.warning( + f"Warning: Zero-padding as not enough data to fill window overlap ( for win_len_s = {self.win_len_s}, and win_step_inc_s = {self.win_step_inc_s})" + ) except IndexError: # Deal with if a particular station has no data for given window: - data[i,j,:] = 0. - return data + data[i, j, :] = 0.0 + return data def _stack_results(self, Pfreq_all): """Function to perform stacking of the results.""" - Psum_all = np.zeros((Pfreq_all.shape[0], Pfreq_all.shape[2], Pfreq_all.shape[3]), dtype=complex) + Psum_all = np.zeros( + (Pfreq_all.shape[0], Pfreq_all.shape[2], Pfreq_all.shape[3]), dtype=complex + ) # Loop over time windows: for i in range(Pfreq_all.shape[0]): if self.norm_pre_stacking: - Pfreq_norm_curr = Pfreq_all[i,:,:,:] / np.sum(np.abs(Pfreq_all[i,:,:,:]), axis=0) - Psum_all[i,:,:] = np.sum(Pfreq_norm_curr,axis=0) + Pfreq_norm_curr = Pfreq_all[i, :, :, :] / np.sum( + np.abs(Pfreq_all[i, :, :, :]), axis=0 + ) + Psum_all[i, :, :] = np.sum(Pfreq_norm_curr, axis=0) else: - Psum_all[i,:,:] = np.sum(Pfreq_all[i,:,:,:],axis=0) + Psum_all[i, :, :] = np.sum(Pfreq_all[i, :, :, :], axis=0) return Psum_all def _find_time_series(self, Psum_all): - """Function to calculate beamforming time-series outputs, given + """Function to calculate beamforming time-series outputs, given a raw beamforming result. Note that the time-series timestamps are in the middle of the time- wimdows. @@ -880,11 +1082,17 @@ def _find_time_series(self, Psum_all): Returns time-series of coherency (power), slowness and back-azimuth. """ # Calcualte ux, uy: - ur = np.linspace(0, self.max_sl,Psum_all.shape[1]) - utheta = utheta = np.linspace(0,360-(360/Psum_all.shape[2]),Psum_all.shape[2]) + ur = np.linspace(0, self.max_sl, Psum_all.shape[1]) + utheta = utheta = np.linspace( + 0, 360 - (360 / Psum_all.shape[2]), Psum_all.shape[2] + ) # Create time-series: n_win_curr = Psum_all.shape[0] - t_series = np.arange(self.win_step_inc_s/2,(n_win_curr*self.win_step_inc_s) + (self.win_step_inc_s/2), self.win_step_inc_s) + t_series = np.arange( + self.win_step_inc_s / 2, + (n_win_curr * self.win_step_inc_s) + (self.win_step_inc_s / 2), + self.win_step_inc_s, + ) if len(t_series) > n_win_curr: t_series = t_series[0:n_win_curr] # And find power, slowness and back-azimuth: @@ -894,41 +1102,56 @@ def _find_time_series(self, Psum_all): # Loop over windows in time: for i in range(n_win_curr): # Calculate max. power: - powers[i] = np.max(np.abs(Psum_all[i,:,:])) + powers[i] = np.max(np.abs(Psum_all[i, :, :])) # Calculate slowness: - r_idx = np.where(Psum_all[i,:,:] == Psum_all[i,:,:].max())[0][0] - theta_idx = np.where(Psum_all[i,:,:] == Psum_all[i,:,:].max())[1][0] + r_idx = np.where(Psum_all[i, :, :] == Psum_all[i, :, :].max())[0][0] + theta_idx = np.where(Psum_all[i, :, :] == Psum_all[i, :, :].max())[1][0] slownesses[i] = ur[r_idx] # And calculate back-azimuth: back_azis[i] = utheta[theta_idx] - - return t_series, powers, slownesses, back_azis + return t_series, powers, slownesses, back_azis def _beamforming(self, st_trimmed, verbosity=0): - """Function to perform beamforming, given a stream of data for a specific + """Function to perform beamforming, given a stream of data for a specific time-window. Function is primarily called by run_array_proc(). Returns (stacked 2D power-slowness space data).""" # Run heavy array processing algorithm: # Specify various variables needed: # Make a linear spacing of frequencies. One might use periods, logspacing, etc.: # (Note: linspace much less noisy than logspace) - target_freqs = np.linspace(self.freqmin,self.freqmax,self.num_freqs) #np.logspace(self.freqmin,self.freqmax,self.num_freqs) + target_freqs = np.linspace( + self.freqmin, self.freqmax, self.num_freqs + ) # np.logspace(self.freqmin,self.freqmax,self.num_freqs) data = self._convert_st_to_np_data(st_trimmed) # Station locations: - xx = self.stations_df['x_array_coords_km'].values - yy = self.stations_df['y_array_coords_km'].values + xx = self.stations_df["x_array_coords_km"].values + yy = self.stations_df["y_array_coords_km"].values # And run: - if verbosity>1: - logger.info("Performing run for",data.shape[0],"windows") + if verbosity > 1: + logger.info("Performing run for", data.shape[0], "windows") tic = time.time() - Pfreq_all = _fast_freq_domain_array_proc(data, self.min_sl, self.max_sl, self.n_sl, self.min_baz, self.max_baz, self.n_baz, - self.fs, target_freqs, xx, yy, self.n_stations, self.n_t_samp, self.remove_autocorr) - if verbosity>1: + Pfreq_all = _fast_freq_domain_array_proc( + data, + self.min_sl, + self.max_sl, + self.n_sl, + self.min_baz, + self.max_baz, + self.n_baz, + self.fs, + target_freqs, + xx, + yy, + self.n_stations, + self.n_t_samp, + self.remove_autocorr, + ) + if verbosity > 1: toc = time.time() - logger.info(f'runtime for _beamforming is {toc-tic}') + logger.info(f"runtime for _beamforming is {toc-tic}") # And tidy: - del data + del data gc.collect() # And remove any data where stations don't exist: @@ -951,124 +1174,170 @@ def _calculate_mad(self, x, scale=1.4826): mad = np.median(np.abs(x - np.median(x))) return scale * mad - def plot_polar_slowness_space(self, beam_power, event_phase_arr_time, component, log=False): + def plot_polar_slowness_space( + self, beam_power, event_phase_arr_time, component, log=False + ): fig = plt.figure() - ax = fig.add_subplot(111, projection='polar') + ax = fig.add_subplot(111, projection="polar") rad = np.linspace(self.min_sl, self.max_sl, beam_power.shape[0]) - azm = np.linspace(np.radians(self.min_baz), - np.radians(self.max_baz), beam_power.shape[1]) + azm = np.linspace( + np.radians(self.min_baz), np.radians(self.max_baz), beam_power.shape[1] + ) th, r = np.meshgrid(azm, rad) - ax.set_theta_offset(np.pi/2) + ax.set_theta_offset(np.pi / 2) ax.set_theta_direction(-1) if log: - im = ax.pcolormesh(th, r, np.log(beam_power), cmap='magma') + im = ax.pcolormesh(th, r, np.log(beam_power), cmap="magma") else: - im = ax.pcolormesh(th, r, beam_power, cmap='magma') + im = ax.pcolormesh(th, r, beam_power, cmap="magma") plt.colorbar(im) plt.grid() - event_date_stamp = f'{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}' - event_time_stamp = f'{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}' - vesp_figpath = Path(self.outdir, 'plots', 'vespagrams') - vesp_figpath.mkdir(parents=True, exist_ok=True) # makes plots/vespagrams if it doesnt exist - fig.savefig(f'{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_{component}.png', dpi=600) + event_date_stamp = f"{event_phase_arr_time.year:04d}{event_phase_arr_time.month:02d}{event_phase_arr_time.day:02d}" + event_time_stamp = f"{event_phase_arr_time.hour:02d}{event_phase_arr_time.minute:02d}{event_phase_arr_time.second:02d}" + vesp_figpath = Path(self.outdir, "plots", "vespagrams") + vesp_figpath.mkdir( + parents=True, exist_ok=True + ) # makes plots/vespagrams if it doesnt exist + fig.savefig( + f"{vesp_figpath}/Detected_event_{event_date_stamp}_{event_time_stamp}_slow_spac_{component}.png", + dpi=600, + ) plt.close() - - def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosity=0): + + def _calc_uncertainties( + self, events_df, t_series_df_Z, t_series_df_hor, verbosity=0 + ): """Function to calculate uncertainties for phase-associated event detections. - Calculates uncertainty in t1, t2, slow1, slow2 and bazi1 and bazi2, - assuming Gaussian uncertainties. Uncertainties are defined as the full-width half - maximum (used due to more efficient optimisation than Gaussian fitting, but potentially + Calculates uncertainty in t1, t2, slow1, slow2 and bazi1 and bazi2, + assuming Gaussian uncertainties. Uncertainties are defined as the full-width half + maximum (used due to more efficient optimisation than Gaussian fitting, but potentially over-estimates error).""" # Do preliminary prep. once: # Define temparory datastore: - uncertainties_df = pd.DataFrame({'t1_err': [], 't2_err': [], 'slow1_err': [], 'slow2_err': [], - 'bazi1_err': [], 'bazi2_err': []}) + uncertainties_df = pd.DataFrame( + { + "t1_err": [], + "t2_err": [], + "slow1_err": [], + "slow2_err": [], + "bazi1_err": [], + "bazi2_err": [], + } + ) # Find max. timeshift (for determining beamforming window): - max_t_shift = self.max_sl * ( np.max(np.abs((self.stations_df['x_array_coords_km'].values))) - + np.max(np.abs((self.stations_df['x_array_coords_km'].values))) ) # (effectively d/v) - n_wins_for_max_t_shift = int(np.ceil(max_t_shift / self.win_step_inc_s)) #+ 1 #(+1 just to ensure that window is definitely wide enough) + max_t_shift = self.max_sl * ( + np.max(np.abs((self.stations_df["x_array_coords_km"].values))) + + np.max(np.abs((self.stations_df["x_array_coords_km"].values))) + ) # (effectively d/v) + n_wins_for_max_t_shift = int( + np.ceil(max_t_shift / self.win_step_inc_s) + ) # + 1 #(+1 just to ensure that window is definitely wide enough) # And loop over detected events, calculating uncertainty: count = 0 for index, row in events_df.iterrows(): if count % 10 == 0: if verbosity > 0: - logger.info("Calculating uncertainty for event", count+1, "/", len(events_df)) + logger.info( + "Calculating uncertainty for event", + count + 1, + "/", + len(events_df), + ) # Load in data (if needed): # (done like this to avoid unnneccessary read ins, improving eff.) - event_phase_arr_time = obspy.UTCDateTime(row['t1']) + event_phase_arr_time = obspy.UTCDateTime(row["t1"]) if count == 0: - st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour) + st = self._load_data( + event_phase_arr_time.year, + event_phase_arr_time.month, + event_phase_arr_time.day, + hour=event_phase_arr_time.hour, + ) # Find uncertainties: # ------- For vertical -------: # Time uncertainty: # Find FWHM for t1 pick: # (only use ascending currently (assume symetric pdf)) - t1_pick_idx = t_series_df_Z.index[t_series_df_Z['t'] == row['t1']][0] - Pxx_curr = t_series_df_Z.iloc[t1_pick_idx]['power'] + t1_pick_idx = t_series_df_Z.index[t_series_df_Z["t"] == row["t1"]][0] + Pxx_curr = t_series_df_Z.iloc[t1_pick_idx]["power"] idx_diff = 0 - while Pxx_curr > t_series_df_Z.iloc[t1_pick_idx]['power'] / 2.: - idx_diff+=1 - Pxx_curr = t_series_df_Z.iloc[t1_pick_idx+idx_diff]['power'] - t1_err = obspy.UTCDateTime(t_series_df_Z.iloc[t1_pick_idx+idx_diff]['t']) - obspy.UTCDateTime(t_series_df_Z.iloc[t1_pick_idx]['t']) - + while Pxx_curr > t_series_df_Z.iloc[t1_pick_idx]["power"] / 2.0: + idx_diff += 1 + Pxx_curr = t_series_df_Z.iloc[t1_pick_idx + idx_diff]["power"] + t1_err = obspy.UTCDateTime( + t_series_df_Z.iloc[t1_pick_idx + idx_diff]["t"] + ) - obspy.UTCDateTime(t_series_df_Z.iloc[t1_pick_idx]["t"]) + # Spatial uncertainty: # (slowness, bazi) # Perform beamforming again around event, to estimate bazi and slowness errs: # Get data: - event_phase_arr_time = obspy.UTCDateTime(row['t1']) + event_phase_arr_time = obspy.UTCDateTime(row["t1"]) # Reload data if needed: - if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: - st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour) + if ( + st[0].stats.starttime > event_phase_arr_time + or st[0].stats.endtime < event_phase_arr_time + ): + st = self._load_data( + event_phase_arr_time.year, + event_phase_arr_time.month, + event_phase_arr_time.day, + hour=event_phase_arr_time.hour, + ) st_trimmed = st.copy() - st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), - endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) + st_trimmed.trim( + starttime=event_phase_arr_time + - ((n_wins_for_max_t_shift + 0.5) * self.win_len_s), + endtime=event_phase_arr_time + + ((n_wins_for_max_t_shift + 0.5) * self.win_len_s), + ) # (Note: 0.5 as windows centred) # Run array processing: # (to get power in polar slowness space) # (Note that need to run for a number of windows, to allow for adequate shifting of data) - self.channel_curr = self.channels_to_use[0] # Do for vertical first + self.channel_curr = self.channels_to_use[0] # Do for vertical first Psum_all = self._beamforming(st_trimmed, verbosity=verbosity) - del st_trimmed + del st_trimmed gc.collect() # Find highest power slowness space for event: t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all) max_idx = np.argmax(powers) - Psum_opt = np.abs(Psum_all[max_idx,:,:]) + Psum_opt = np.abs(Psum_all[max_idx, :, :]) # Find FWHM for slowness and bazi: - slow_idx_peak = np.where(Psum_opt==np.max(Psum_opt))[0][0] - bazi_idx_peak = np.where(Psum_opt==np.max(Psum_opt))[1][0] + slow_idx_peak = np.where(Psum_opt == np.max(Psum_opt))[0][0] + bazi_idx_peak = np.where(Psum_opt == np.max(Psum_opt))[1][0] # Slowness: # (go radially outwards for slowness, assumes symetric or sharper gradient inwards) Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak] idx_diff = 0 - while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.: - idx_diff+=1 - if slow_idx_peak+idx_diff < Psum_opt.shape[0]: - Pxx_curr = Psum_opt[slow_idx_peak+idx_diff, bazi_idx_peak] + while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.0: + idx_diff += 1 + if slow_idx_peak + idx_diff < Psum_opt.shape[0]: + Pxx_curr = Psum_opt[slow_idx_peak + idx_diff, bazi_idx_peak] else: - Pxx_curr = 0 # Force exit if reach slowness limits - dslow = self.max_sl / Psum_opt.shape[0] # Assumes linear slowness space + Pxx_curr = 0 # Force exit if reach slowness limits + dslow = self.max_sl / Psum_opt.shape[0] # Assumes linear slowness space slow1_err = idx_diff * dslow # Back-azimuth: # (go clockwise, assuming symetric) Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak] idx_diff = 0 - while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.: - idx_diff+=1 - if bazi_idx_peak+idx_diff < Psum_opt.shape[1]: - Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak+idx_diff] + while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.0: + idx_diff += 1 + if bazi_idx_peak + idx_diff < Psum_opt.shape[1]: + Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak + idx_diff] else: try: - Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak+idx_diff-Psum_opt.shape[1]] # (loop beyond 360 degrees) + Pxx_curr = Psum_opt[ + slow_idx_peak, bazi_idx_peak + idx_diff - Psum_opt.shape[1] + ] # (loop beyond 360 degrees) except IndexError: # Deal with 360 degree error: - Pxx_curr = 0 - idx_diff = Psum_opt.shape[1] + Pxx_curr = 0 + idx_diff = Psum_opt.shape[1] dbazi = 360 / Psum_opt.shape[1] bazi1_err = idx_diff * dbazi @@ -1076,88 +1345,119 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='vert') + self.plot_polar_slowness_space( + Psum_opt, event_phase_arr_time, component="vert" + ) # ------- For horizontal -------: # And find FWHM for t2 pick: # (only use ascending currently (assume symetric pdf)) - t2_pick_idx = t_series_df_hor.index[t_series_df_hor['t'] == row['t2']][0] - Pxx_curr = t_series_df_hor.iloc[t2_pick_idx]['power'] + t2_pick_idx = t_series_df_hor.index[t_series_df_hor["t"] == row["t2"]][0] + Pxx_curr = t_series_df_hor.iloc[t2_pick_idx]["power"] idx_diff = 0 - while Pxx_curr > t_series_df_hor.iloc[t2_pick_idx]['power'] / 2.: - idx_diff+=1 - Pxx_curr = t_series_df_hor.iloc[t2_pick_idx+idx_diff]['power'] - t2_err = obspy.UTCDateTime(t_series_df_hor.iloc[t2_pick_idx+idx_diff]['t']) - obspy.UTCDateTime(t_series_df_hor.iloc[t2_pick_idx]['t']) + while Pxx_curr > t_series_df_hor.iloc[t2_pick_idx]["power"] / 2.0: + idx_diff += 1 + Pxx_curr = t_series_df_hor.iloc[t2_pick_idx + idx_diff]["power"] + t2_err = obspy.UTCDateTime( + t_series_df_hor.iloc[t2_pick_idx + idx_diff]["t"] + ) - obspy.UTCDateTime(t_series_df_hor.iloc[t2_pick_idx]["t"]) # Spatial uncertainty: # (slowness, bazi) # Perform beamforming again around event, to estimate bazi and slowness errs: # Get data: - event_phase_arr_time = obspy.UTCDateTime(row['t2']) + event_phase_arr_time = obspy.UTCDateTime(row["t2"]) # Reload data if needed: - if st[0].stats.starttime > event_phase_arr_time or st[0].stats.endtime < event_phase_arr_time: - st = self._load_data(event_phase_arr_time.year, event_phase_arr_time.month, - event_phase_arr_time.day, hour=event_phase_arr_time.hour) + if ( + st[0].stats.starttime > event_phase_arr_time + or st[0].stats.endtime < event_phase_arr_time + ): + st = self._load_data( + event_phase_arr_time.year, + event_phase_arr_time.month, + event_phase_arr_time.day, + hour=event_phase_arr_time.hour, + ) st_trimmed = st.copy() - st_trimmed.trim(starttime=event_phase_arr_time-((n_wins_for_max_t_shift+0.5)*self.win_len_s), - endtime=event_phase_arr_time+((n_wins_for_max_t_shift+0.5)*self.win_len_s)) # (Note: 0.5 as windows centred) + st_trimmed.trim( + starttime=event_phase_arr_time + - ((n_wins_for_max_t_shift + 0.5) * self.win_len_s), + endtime=event_phase_arr_time + + ((n_wins_for_max_t_shift + 0.5) * self.win_len_s), + ) # (Note: 0.5 as windows centred) # Run array processing: # (to get power in polar slowness space) # (Note that need to run for a number of windows, to allow for adequate shifting of data) - self.channel_curr = self.channels_to_use[1] # Do for vertical first + self.channel_curr = self.channels_to_use[1] # Do for vertical first Psum_all_N = self._beamforming(st_trimmed, verbosity=verbosity) - self.channel_curr = self.channels_to_use[2] # Do for vertical first + self.channel_curr = self.channels_to_use[2] # Do for vertical first Psum_all_E = self._beamforming(st_trimmed, verbosity=verbosity) - del st_trimmed + del st_trimmed gc.collect() Psum_all_NE = Psum_all_N + Psum_all_E # Find highest power slowness space for event: - t_series, powers, slownesses, back_azis = self._find_time_series(Psum_all_NE) + t_series, powers, slownesses, back_azis = self._find_time_series( + Psum_all_NE + ) max_idx = np.argmax(powers) - Psum_opt = np.abs(Psum_all_NE[max_idx,:,:]) + Psum_opt = np.abs(Psum_all_NE[max_idx, :, :]) # Find FWHM for slowness and bazi: - slow_idx_peak = np.where(Psum_opt==np.max(Psum_opt))[0][0] - bazi_idx_peak = np.where(Psum_opt==np.max(Psum_opt))[1][0] + slow_idx_peak = np.where(Psum_opt == np.max(Psum_opt))[0][0] + bazi_idx_peak = np.where(Psum_opt == np.max(Psum_opt))[1][0] # Slowness: # (go radially outwards for slowness, assumes symetric or sharper gradient inwards) Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak] idx_diff = 0 - while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.: - idx_diff+=1 - if slow_idx_peak+idx_diff < Psum_opt.shape[0]: - Pxx_curr = Psum_opt[slow_idx_peak+idx_diff, bazi_idx_peak] + while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.0: + idx_diff += 1 + if slow_idx_peak + idx_diff < Psum_opt.shape[0]: + Pxx_curr = Psum_opt[slow_idx_peak + idx_diff, bazi_idx_peak] else: - Pxx_curr = 0 # Force exit if reach slowness limits - dslow = self.max_sl / Psum_opt.shape[0] # Assumes linear slowness space + Pxx_curr = 0 # Force exit if reach slowness limits + dslow = self.max_sl / Psum_opt.shape[0] # Assumes linear slowness space slow2_err = idx_diff * dslow # Back-azimuth: # (go clockwise, assuming symetric) Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak] idx_diff = 0 - while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.: - idx_diff+=1 - if bazi_idx_peak+idx_diff < Psum_opt.shape[1]: - Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak+idx_diff] + while Pxx_curr > Psum_opt[slow_idx_peak, bazi_idx_peak] / 2.0: + idx_diff += 1 + if bazi_idx_peak + idx_diff < Psum_opt.shape[1]: + Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak + idx_diff] else: try: - Pxx_curr = Psum_opt[slow_idx_peak, bazi_idx_peak+idx_diff-Psum_opt.shape[1]] # (loop beyond 360 degrees) + Pxx_curr = Psum_opt[ + slow_idx_peak, bazi_idx_peak + idx_diff - Psum_opt.shape[1] + ] # (loop beyond 360 degrees) except IndexError: # Deal with 360 degree error: - Pxx_curr = 0 + Pxx_curr = 0 idx_diff = Psum_opt.shape[1] - dbazi = 360. / Psum_opt.shape[1] + dbazi = 360.0 / Psum_opt.shape[1] bazi2_err = idx_diff * dbazi # ------- End horizontal ------- - + # Plot slowness space that used for uncertainty, if specified: if verbosity >= 1: - self.plot_polar_slowness_space(Psum_opt, event_phase_arr_time, component='horz') + self.plot_polar_slowness_space( + Psum_opt, event_phase_arr_time, component="horz" + ) # And append data to overall uncertainties df: - uncertainties_df_curr = pd.DataFrame({'t1_err': [t1_err], 't2_err': [t2_err], 'slow1_err': [slow1_err], - 'slow2_err': [slow2_err], 'bazi1_err': [bazi1_err], 'bazi2_err': [bazi2_err]}) - uncertainties_df = pd.concat([uncertainties_df, uncertainties_df_curr], ignore_index=True) + uncertainties_df_curr = pd.DataFrame( + { + "t1_err": [t1_err], + "t2_err": [t2_err], + "slow1_err": [slow1_err], + "slow2_err": [slow2_err], + "bazi1_err": [bazi1_err], + "bazi2_err": [bazi2_err], + } + ) + uncertainties_df = pd.concat( + [uncertainties_df, uncertainties_df_curr], ignore_index=True + ) # And update count: - count+=1 + count += 1 # And add uncertainties to events_df: events_df = events_df.reset_index(drop=True) @@ -1167,8 +1467,8 @@ def _calc_uncertainties(self, events_df, t_series_df_Z, t_series_df_hor, verbosi return events_df def detect_events(self, verbosity=0, fnames=None): - """Function to detect events, based on the power time-series generated - by run_array_proc(). Note: Currently, only Median Absolute Deviation + """Function to detect events, based on the power time-series generated + by run_array_proc(). Note: Currently, only Median Absolute Deviation triggering is implemented. Key attributes used are: - mad_window_length_s @@ -1180,7 +1480,9 @@ def detect_events(self, verbosity=0, fnames=None): events_df_all = pd.DataFrame() # Loop over array proc outdir data: if fnames is None: - fnames = glob.glob(os.path.join(self.outdir, "detection_t_series_*_chZ.csv")) + fnames = glob.glob( + os.path.join(self.outdir, "detection_t_series_*_chZ.csv") + ) for fname in fnames: f_uid = fname[-21:-8] # Check if in list to process: @@ -1190,35 +1492,58 @@ def detect_events(self, verbosity=0, fnames=None): t_series_df_Z = pd.read_csv(fname) # And read in horizontals: try: - fname_N = os.path.join(self.outdir, ''.join(( "detection_t_series_", f_uid, "_chN.csv" ))) + fname_N = os.path.join( + self.outdir, "".join(("detection_t_series_", f_uid, "_chN.csv")) + ) t_series_df_N = pd.read_csv(fname_N) except FileNotFoundError: - fname_N = os.path.join(self.outdir, ''.join(( "detection_t_series_", f_uid, "_ch1.csv" ))) + fname_N = os.path.join( + self.outdir, "".join(("detection_t_series_", f_uid, "_ch1.csv")) + ) t_series_df_N = pd.read_csv(fname_N) try: - fname_E = os.path.join(self.outdir, ''.join(( "detection_t_series_", f_uid, "_chE.csv" ))) + fname_E = os.path.join( + self.outdir, "".join(("detection_t_series_", f_uid, "_chE.csv")) + ) t_series_df_E = pd.read_csv(fname_E) except FileNotFoundError: - fname_E = os.path.join(self.outdir, ''.join(( "detection_t_series_", f_uid, "_ch2.csv" ))) + fname_E = os.path.join( + self.outdir, "".join(("detection_t_series_", f_uid, "_ch2.csv")) + ) t_series_df_E = pd.read_csv(fname_E) else: - logger.warning(f'fname {fname} not in fname_array_proc list') - logger.warning(f'fname_array_proc first entry looks like this {self.out_fnames_array_proc[0]}') - continue # Skip file, as not previously been processed. + logger.warning(f"fname {fname} not in fname_array_proc list") + logger.warning( + f"fname_array_proc first entry looks like this {self.out_fnames_array_proc[0]}" + ) + continue # Skip file, as not previously been processed. # And check to see that t-series exists within file: if len(t_series_df_Z) == 0: - continue + continue if len(t_series_df_N) == 0: - continue + continue if len(t_series_df_E) == 0: - continue + continue # Check if all inputs are same length, and if not, skip file: if not len(t_series_df_Z) == len(t_series_df_N) == len(t_series_df_E): - logger.warning("Warning: Files with f uid", f_uid, - "are not of equal length. Therefore using shortest length (will miss some data).") - logger.warning("( Lengths are", len(t_series_df_Z) , len(t_series_df_N) , len(t_series_df_E), ")") - min_len = np.min(np.array([len(t_series_df_Z), len(t_series_df_N), len(t_series_df_E)])) + logger.warning( + "Warning: Files with f uid", + f_uid, + "are not of equal length. Therefore using shortest length (will miss some data).", + ) + logger.warning( + "( Lengths are", + len(t_series_df_Z), + len(t_series_df_N), + len(t_series_df_E), + ")", + ) + min_len = np.min( + np.array( + [len(t_series_df_Z), len(t_series_df_N), len(t_series_df_E)] + ) + ) t_series_df_Z = t_series_df_Z.iloc[:min_len] t_series_df_N = t_series_df_N.iloc[:min_len] t_series_df_E = t_series_df_E.iloc[:min_len] @@ -1226,37 +1551,79 @@ def detect_events(self, verbosity=0, fnames=None): # Combine horizontals: # (Using RMS of N and E signals for slowness and average for BAZI) t_series_df_hor = t_series_df_N.copy() - t_series_df_hor["power"] = np.sqrt(t_series_df_N["power"].values**2 + t_series_df_E["power"].values**2) - NE_Pxx_max = np.max(np.concatenate((t_series_df_N["power"].values, t_series_df_E["power"].values))) + t_series_df_hor["power"] = np.sqrt( + t_series_df_N["power"].values ** 2 + t_series_df_E["power"].values ** 2 + ) + NE_Pxx_max = np.max( + np.concatenate( + (t_series_df_N["power"].values, t_series_df_E["power"].values) + ) + ) N_weighting = t_series_df_N["power"].values / NE_Pxx_max E_weighting = t_series_df_E["power"].values / NE_Pxx_max - t_series_df_hor["slowness"] = np.sqrt(np.average(np.vstack((t_series_df_N['slowness']**2, t_series_df_E['slowness']**2)), - axis=0, weights=np.vstack((N_weighting, - E_weighting)))) # Weighted mean (weighted by power) - t_series_df_hor["back_azi"] = np.average(np.vstack((t_series_df_N['back_azi'], t_series_df_E['back_azi'])), - axis=0, weights=np.vstack((N_weighting, - E_weighting))) # Weighted mean (weighted by power) + t_series_df_hor["slowness"] = np.sqrt( + np.average( + np.vstack( + (t_series_df_N["slowness"] ** 2, t_series_df_E["slowness"] ** 2) + ), + axis=0, + weights=np.vstack((N_weighting, E_weighting)), + ) + ) # Weighted mean (weighted by power) + t_series_df_hor["back_azi"] = np.average( + np.vstack((t_series_df_N["back_azi"], t_series_df_E["back_azi"])), + axis=0, + weights=np.vstack((N_weighting, E_weighting)), + ) # Weighted mean (weighted by power) # print("(Weighted horizontal slowness and back-azi using power)") del N_weighting, E_weighting, t_series_df_N, t_series_df_E gc.collect() # Calculate pick thresholds: - mad_pick_threshold_Z = np.median(t_series_df_Z['power'].values) + (self.mad_multiplier * self._calculate_mad(t_series_df_Z['power'])) - mad_pick_threshold_hor = np.median(t_series_df_hor['power'].values) + (self.mad_multiplier * self._calculate_mad(t_series_df_hor['power'])) - + mad_pick_threshold_Z = np.median(t_series_df_Z["power"].values) + ( + self.mad_multiplier * self._calculate_mad(t_series_df_Z["power"]) + ) + mad_pick_threshold_hor = np.median(t_series_df_hor["power"].values) + ( + self.mad_multiplier * self._calculate_mad(t_series_df_hor["power"]) + ) + # Get phase picks: - min_pick_dist = int(self.min_event_sep_s / (obspy.UTCDateTime(t_series_df_Z['t'][1]) - obspy.UTCDateTime(t_series_df_Z['t'][0]))) - peaks_Z, _ = find_peaks(t_series_df_Z['power'].values, height=mad_pick_threshold_Z, distance=min_pick_dist) - peaks_hor, _ = find_peaks(t_series_df_hor['power'].values, height=mad_pick_threshold_hor, distance=min_pick_dist) + min_pick_dist = int( + self.min_event_sep_s + / ( + obspy.UTCDateTime(t_series_df_Z["t"][1]) + - obspy.UTCDateTime(t_series_df_Z["t"][0]) + ) + ) + peaks_Z, _ = find_peaks( + t_series_df_Z["power"].values, + height=mad_pick_threshold_Z, + distance=min_pick_dist, + ) + peaks_hor, _ = find_peaks( + t_series_df_hor["power"].values, + height=mad_pick_threshold_hor, + distance=min_pick_dist, + ) # Phase assoicate by BAZI threshold and max. power: - events_df = _phase_associator(t_series_df_Z, t_series_df_hor, peaks_Z, peaks_hor, - self.bazi_tol, self.filt_phase_assoc_by_max_power, - self.max_phase_sep_s, self.min_event_sep_s, verbosity=verbosity) + events_df = _phase_associator( + t_series_df_Z, + t_series_df_hor, + peaks_Z, + peaks_hor, + self.bazi_tol, + self.filt_phase_assoc_by_max_power, + self.max_phase_sep_s, + self.min_event_sep_s, + verbosity=verbosity, + ) # Find uncertainties (in time, bazi, slowness): if self.calc_uncertainties: - events_df = self._calc_uncertainties(events_df, t_series_df_Z, t_series_df_hor, verbosity=verbosity) + events_df = self._calc_uncertainties( + events_df, t_series_df_Z, t_series_df_hor, verbosity=verbosity + ) # Append to datastore: events_df_all = pd.concat([events_df_all, events_df]) @@ -1264,22 +1631,54 @@ def detect_events(self, verbosity=0, fnames=None): # Plot detected, phase-associated picks: if verbosity > 1: # print("="*40) - logger.info("Event phase associations:") + logger.info("Event phase associations:") # print(events_df) # print("="*40) - fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(9,6)) + fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(9, 6)) # Plot power: - ax[0].plot(t_series_df_Z['t'], t_series_df_Z['power'], label="Vertical power") - ax[0].plot(t_series_df_hor['t'], t_series_df_hor['power'], label="Horizontal power") + ax[0].plot( + t_series_df_Z["t"], t_series_df_Z["power"], label="Vertical power" + ) + ax[0].plot( + t_series_df_hor["t"], + t_series_df_hor["power"], + label="Horizontal power", + ) # Plot slowness: - ax[1].plot(t_series_df_Z['t'], t_series_df_Z['slowness'], label="Vertical slowness") - ax[1].plot(t_series_df_hor['t'], t_series_df_hor['slowness'], label="Horizontal slowness") + ax[1].plot( + t_series_df_Z["t"], + t_series_df_Z["slowness"], + label="Vertical slowness", + ) + ax[1].plot( + t_series_df_hor["t"], + t_series_df_hor["slowness"], + label="Horizontal slowness", + ) # Plot back-azimuth: - ax[2].plot(t_series_df_Z['t'], t_series_df_Z['back_azi'], label="Vertical back-azimuth") - ax[2].plot(t_series_df_hor['t'], t_series_df_hor['back_azi'], label="Horizontal back-azimuth") + ax[2].plot( + t_series_df_Z["t"], + t_series_df_Z["back_azi"], + label="Vertical back-azimuth", + ) + ax[2].plot( + t_series_df_hor["t"], + t_series_df_hor["back_azi"], + label="Horizontal back-azimuth", + ) if len(events_df_all) > 0: - ax[0].scatter(events_df_all['t1'], np.ones(len(events_df_all))*np.max(t_series_df_Z['power']), c='r', label="P phase picks") - ax[0].scatter(events_df_all['t2'], np.ones(len(events_df_all))*np.max(t_series_df_Z['power']), c='b', label="S phase picks") + ax[0].scatter( + events_df_all["t1"], + np.ones(len(events_df_all)) * np.max(t_series_df_Z["power"]), + c="r", + label="P phase picks", + ) + ax[0].scatter( + events_df_all["t2"], + np.ones(len(events_df_all)) * np.max(t_series_df_Z["power"]), + c="b", + label="S phase picks", + ) else: logger.info("No events to plot.") ax[0].legend() @@ -1287,25 +1686,29 @@ def detect_events(self, verbosity=0, fnames=None): ax[0].set_ylabel("Power (arb. units)") ax[1].set_ylabel("Slowness ($km$ $s^{-1}$)") ax[2].set_ylabel("Back-azimuth ($^o$)") - # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) + # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) for i in range(3): ax[i].xaxis.set_major_locator(plt.MaxNLocator(3)) - figpath = Path(self.outdir, 'plots', 'detection_t_series') - figpath.mkdir(parents=True, exist_ok=True) - fig.savefig(f'{figpath}/Phase_association_{f_uid}.png', dpi=600) + figpath = Path(self.outdir, "plots", "detection_t_series") + figpath.mkdir(parents=True, exist_ok=True) + fig.savefig(f"{figpath}/Phase_association_{f_uid}.png", dpi=600) plt.show() - return events_df_all - - - def create_location_LUTs(self, oneD_vel_model_z_df, extent_x_m=4000, dxz=[100,100], array_centre_xz=[0, 0]): + + def create_location_LUTs( + self, + oneD_vel_model_z_df, + extent_x_m=4000, + dxz=[100, 100], + array_centre_xz=[0, 0], + ): """Function to create lookup tables used for location. Lookup tables created are: - P travel-times - S travel-times. - P inclination angles. - S inclination angles. - Note: Array centre is typically defined as the + Note: Array centre is typically defined as the Parameters ---------- @@ -1318,13 +1721,13 @@ def create_location_LUTs(self, oneD_vel_model_z_df, extent_x_m=4000, dxz=[100,10 Extent of lookup table horizontal. Units are metres. Default is 4000 m. array_centre_xz : list - Path to csv file containing station/receiver locations. Headers need - to be of format: Latitude Longitude Elevation Name. No need to change unless - the user has a particularly good reason. Takes a list of length two, containing + Path to csv file containing station/receiver locations. Headers need + to be of format: Latitude Longitude Elevation Name. No need to change unless + the user has a particularly good reason. Takes a list of length two, containing the array centre in x and z, in metres from the LUT grid origin. dxz : list - List of two floats, defining the spatial spacing of the nodes in the LUTs in x + List of two floats, defining the spatial spacing of the nodes in the LUTs in x and z. Units are metres. Default is [100, 100]. Returns @@ -1340,26 +1743,34 @@ def create_location_LUTs(self, oneD_vel_model_z_df, extent_x_m=4000, dxz=[100,10 # Create 2D LUTs: # (for P and S travel-times, and inclination angle) # And get travel times: - trav_times_grid_P, trav_times_grid_S, theta_grid_P, theta_grid_S, vel_model_x_labels, vel_model_z_labels = lookup_table_manager.create_2D_LUT(oneD_vel_model_z_df, - array_centre_xz, extent_x_m=extent_x_m, dxz=dxz) + ( + trav_times_grid_P, + trav_times_grid_S, + theta_grid_P, + theta_grid_S, + vel_model_x_labels, + vel_model_z_labels, + ) = lookup_table_manager.create_2D_LUT( + oneD_vel_model_z_df, array_centre_xz, extent_x_m=extent_x_m, dxz=dxz + ) # And save outputs: LUT_outdir = os.path.join(self.outdir, "LUT") os.makedirs(LUT_outdir, exist_ok=True) LUTs_dict = {} - LUTs_dict['trav_times_grid_P'] = trav_times_grid_P - LUTs_dict['trav_times_grid_S'] = trav_times_grid_S - LUTs_dict['theta_grid_P'] = theta_grid_P - LUTs_dict['theta_grid_S'] = theta_grid_S - LUTs_dict['vel_model_x_labels'] = vel_model_x_labels - LUTs_dict['vel_model_z_labels'] = vel_model_z_labels - LUTs_dict['oneD_vel_model_z_df'] = self.oneD_vel_model_z_df - LUTs_dict['extent_x_m'] = self.extent_x_m - LUTs_dict['dxz'] = self.dxz - LUTs_dict['array_centre_xz'] = self.array_centre_xz - self.LUTs_fname = os.path.join(LUT_outdir, 'LUTs.pkl') - pickle.dump( LUTs_dict, open( self.LUTs_fname, "wb" ) ) + LUTs_dict["trav_times_grid_P"] = trav_times_grid_P + LUTs_dict["trav_times_grid_S"] = trav_times_grid_S + LUTs_dict["theta_grid_P"] = theta_grid_P + LUTs_dict["theta_grid_S"] = theta_grid_S + LUTs_dict["vel_model_x_labels"] = vel_model_x_labels + LUTs_dict["vel_model_z_labels"] = vel_model_z_labels + LUTs_dict["oneD_vel_model_z_df"] = self.oneD_vel_model_z_df + LUTs_dict["extent_x_m"] = self.extent_x_m + LUTs_dict["dxz"] = self.dxz + LUTs_dict["array_centre_xz"] = self.array_centre_xz + self.LUTs_fname = os.path.join(LUT_outdir, "LUTs.pkl") + pickle.dump(LUTs_dict, open(self.LUTs_fname, "wb")) print("Saved LUT to:", self.LUTs_fname) - self.LUTs_dict = LUTs_dict + self.LUTs_dict = LUTs_dict return LUTs_dict def load_location_LUTs(self, LUTs_fname=None): @@ -1367,23 +1778,22 @@ def load_location_LUTs(self, LUTs_fname=None): Parameters ---------- LUTs_fname : str - Path to LUT file to load. Optional. Default is to use the attribute - . + Path to LUT file to load. Optional. Default is to use the attribute + . """ # Assign attribute, if not already specified correctly: if LUTs_fname: self.LUTs_fname = LUTs_fname # And load LUTs: - LUTs_dict = pickle.load( open( self.LUTs_fname, "rb" ) ) + LUTs_dict = pickle.load(open(self.LUTs_fname, "rb")) # And assign relevent attributes: - self.oneD_vel_model_z_df = LUTs_dict['oneD_vel_model_z_df'] - self.extent_x_m = LUTs_dict['extent_x_m'] - self.dxz = LUTs_dict['dxz'] - self.array_centre_xz = LUTs_dict['array_centre_xz'] - self.LUTs_dict = LUTs_dict + self.oneD_vel_model_z_df = LUTs_dict["oneD_vel_model_z_df"] + self.extent_x_m = LUTs_dict["extent_x_m"] + self.dxz = LUTs_dict["dxz"] + self.array_centre_xz = LUTs_dict["array_centre_xz"] + self.LUTs_dict = LUTs_dict return LUTs_dict - def locate_events(self, events_df, verbosity=0): """Function to locate events using LUT.""" # Perform tests to check that various required attributes are specified: @@ -1403,44 +1813,58 @@ def locate_events(self, events_df, verbosity=0): # Locate events: if not exit_bool: - events_df = location.locate_events_from_P_and_S_array_arrivals(events_df, self.LUTs_dict, self.array_latlon, - self.receiver_vp, self.receiver_vs, - verbosity=verbosity) - + events_df = location.locate_events_from_P_and_S_array_arrivals( + events_df, + self.LUTs_dict, + self.array_latlon, + self.receiver_vp, + self.receiver_vs, + verbosity=verbosity, + ) + return events_df - def save(self, out_fname=None): """Function to save class object to file. Parameters ---------- out_fname : str - Path to save class object to. Optional. If not specified, then save to + Path to save class object to. Optional. If not specified, then save to /detect_obj.pkl. """ # Save class to file: if not out_fname: out_fname = os.path.join(self.outdir, "detect_obj.pkl") - f = open(out_fname, 'wb') + f = open(out_fname, "wb") pickle.dump(self.__dict__, f) f.close() print("Saved detection instance to:", out_fname) - + def load(self, preload_fname): """try load self.name.txt""" - f = open(preload_fname, 'rb') - self.__dict__ = pickle.load(f) + f = open(preload_fname, "rb") + self.__dict__ = pickle.load(f) f.close() print("Loaded detection instance from:", preload_fname) - - def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slows_1_2, t_before_s=10, t_after_s=10, st_out_fname='out.m', return_streams=False, method='linear', degree=1): - """Function to find array stacked stream from back-azimuth and slowness. Returns average amplitude + def get_composite_array_st_from_bazi_slowness( + self, + arrival_time, + bazis_1_2, + slows_1_2, + t_before_s=10, + t_after_s=10, + st_out_fname="out.m", + return_streams=False, + method="linear", + degree=1, + ): + """Function to find array stacked stream from back-azimuth and slowness. Returns average amplitude time-series seismogram of stacked array data, for all three componets. Parameters ---------- arrival_time : obspy UTCDateTime object - Arrival time at array. Will output window around this time, as defined by + Arrival time at array. Will output window around this time, as defined by and . bazis_1_2 : list of 2 floats @@ -1450,27 +1874,32 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo Slowness of event phase arrivals in vertical and horizontal, in seconds/km. t_before_s : float - Time, in seconds, before arrival time to include in window. Optional. Default + Time, in seconds, before arrival time to include in window. Optional. Default is 10 s. t_after_s : float - Time, in seconds, after arrival time to include in window. Optional. Default - is 10 s. - + Time, in seconds, after arrival time to include in window. Optional. Default + is 10 s. + st_out_fname : str Filename of to save mseed data stream to. Optional. Default is out.m. return_streams : bool - If True, returns st and composite_st. Optional. Default = False. + If True, returns st and composite_st. Optional. Default = False. """ # Load in raw mseed data: - st = self._load_data(arrival_time.year, arrival_time.month, - arrival_time.day, hour=arrival_time.hour, norm=False) + st = self._load_data( + arrival_time.year, + arrival_time.month, + arrival_time.day, + hour=arrival_time.hour, + norm=False, + ) # And trim data: - st.trim(starttime=arrival_time-t_before_s, endtime=arrival_time+t_after_s) + st.trim(starttime=arrival_time - t_before_s, endtime=arrival_time + t_after_s) # Upsample data soas to provide best time shift later: - st.interpolate(sampling_rate=10*st[0].stats.sampling_rate) + st.interpolate(sampling_rate=10 * st[0].stats.sampling_rate) # Find and perform time shifts for all receivers: # Loop over stations: @@ -1478,23 +1907,29 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo # Find time shift: # Get current station location (relative to array centre): curr_station = st[i].stats.station - x_rec = self.stations_df.loc[self.stations_df['Name'] == curr_station]['x_array_coords_km'].values[0] - y_rec = self.stations_df.loc[self.stations_df['Name'] == curr_station]['y_array_coords_km'].values[0] + x_rec = self.stations_df.loc[self.stations_df["Name"] == curr_station][ + "x_array_coords_km" + ].values[0] + y_rec = self.stations_df.loc[self.stations_df["Name"] == curr_station][ + "y_array_coords_km" + ].values[0] # Select either vertical or horizontal slowness and back-azimuth, depending on component: comp = st[i].stats.channel[-1] - if comp == 'Z': + if comp == "Z": bazi = bazis_1_2[0] slow = slows_1_2[0] - elif comp == 'N' or comp == 'E' or comp == '1' or comp == '2': + elif comp == "N" or comp == "E" or comp == "1" or comp == "2": bazi = bazis_1_2[1] slow = slows_1_2[1] # Calculate arrival time shift relative to centre of array: - time_shift_curr_s = _calc_time_shift_from_array_cent(slow, bazi, x_rec, y_rec) - + time_shift_curr_s = _calc_time_shift_from_array_cent( + slow, bazi, x_rec, y_rec + ) + # And perform time shift on data: n_samp_to_shift = round(time_shift_curr_s * st[i].stats.sampling_rate) st[i].data = np.roll(st[i].data, n_samp_to_shift) - + # And find stacked, mean and stdev of data: n_stat = len(st.select(channel="??Z")) # Get unique channels: @@ -1514,41 +1949,53 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo for i in range(n_stat): # Z: if len(st.select(channel="??Z")[i].data) == max_st_len: - Z_all[:,i] = st.select(channel="??Z")[i].data + Z_all[:, i] = st.select(channel="??Z")[i].data else: - Z_all[:len(st.select(channel="??Z")[i].data),i] = st.select(channel="??Z")[i].data - try: + Z_all[: len(st.select(channel="??Z")[i].data), i] = st.select( + channel="??Z" + )[i].data + try: # N: if len(st.select(channel="??N")[i].data) == max_st_len: - N_all[:,i] = st.select(channel="??N")[i].data + N_all[:, i] = st.select(channel="??N")[i].data else: - N_all[:len(st.select(channel="??N")[i].data),i] = st.select(channel="??N")[i].data + N_all[: len(st.select(channel="??N")[i].data), i] = st.select( + channel="??N" + )[i].data # E: if len(st.select(channel="??E")[i].data) == max_st_len: - E_all[:,i] = st.select(channel="??E")[i].data + E_all[:, i] = st.select(channel="??E")[i].data else: - E_all[:len(st.select(channel="??E")[i].data),i] = st.select(channel="??E")[i].data + E_all[: len(st.select(channel="??E")[i].data), i] = st.select( + channel="??E" + )[i].data except IndexError: # And write if uses 1 and 2 labels rather than N and E: # N: if len(st.select(channel="??1")[i].data) == max_st_len: - N_all[:,i] = st.select(channel="??1")[i].data + N_all[:, i] = st.select(channel="??1")[i].data else: - N_all[:len(st.select(channel="??1")[i].data),i] = st.select(channel="??1")[i].data + N_all[: len(st.select(channel="??1")[i].data), i] = st.select( + channel="??1" + )[i].data # E: if len(st.select(channel="??2")[i].data) == max_st_len: - E_all[:,i] = st.select(channel="??2")[i].data + E_all[:, i] = st.select(channel="??2")[i].data else: - E_all[:len(st.select(channel="??2")[i].data),i] = st.select(channel="??2")[i].data + E_all[: len(st.select(channel="??2")[i].data), i] = st.select( + channel="??2" + )[i].data except: - print('Date gap, continue... for now') + print("Date gap, continue... for now") continue # And create stacked data stream: - if method == 'linear': + if method == "linear": composite_st = _create_stacked_data_st(st, Z_all, N_all, E_all) - elif method == 'pws': - composite_st = _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree) - elif method == 'nth_root': + elif method == "pws": + composite_st = _create_phase_weighted_stack_st( + st, Z_all, N_all, E_all, degree + ) + elif method == "nth_root": pass # composite_st = _create_nth_root_stack_st(st, Z_all, N_all, E_all, degree) # And decimate data back down to original sampling rate: @@ -1558,7 +2005,7 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo # And save data: if st_out_fname: st.write(st_out_fname, format="MSEED") - composite_st_out_fname = st_out_fname.split('.')[0] + "_composite.m" + composite_st_out_fname = st_out_fname.split(".")[0] + "_composite.m" composite_st.write(composite_st_out_fname, format="MSEED") if return_streams: @@ -1568,18 +2015,4 @@ def get_composite_array_st_from_bazi_slowness(self, arrival_time, bazis_1_2, slo gc.collect() - - - - - - - - - - - -#----------------------------------------------- End: Define main functions ----------------------------------------------- - - - +# ----------------------------------------------- End: Define main functions ----------------------------------------------- diff --git a/SeisSeeker/processing/location.py b/SeisSeeker/processing/location.py index 1fe7894..5126083 100755 --- a/SeisSeeker/processing/location.py +++ b/SeisSeeker/processing/location.py @@ -1,35 +1,36 @@ #!/usr/bin/python -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Script Description: # Script to perform earthquake location using array processing methods. # Created by Tom Hudson, 10th August 2022 -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Import neccessary modules: import pandas as pd import numpy as np -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt import matplotlib import os, sys import obspy from scipy.signal import find_peaks from numba import jit, objmode, prange, set_num_threads -import gc -# import multiprocessing as mp -import time +import gc +# import multiprocessing as mp +import time -#----------------------------------------------- Define main functions ----------------------------------------------- +# ----------------------------------------------- Define main functions ----------------------------------------------- class CustomError(Exception): pass - -def locate_events_from_P_and_S_array_arrivals(events_df, LUTs_dict, array_latlon, receiver_vp, receiver_vs, verbosity=0): +def locate_events_from_P_and_S_array_arrivals( + events_df, LUTs_dict, array_latlon, receiver_vp, receiver_vs, verbosity=0 +): """Function to locate events from P and S phase arrivals.""" # Append rows to events_df to save locations: events_df["x_km"] = "" @@ -39,62 +40,66 @@ def locate_events_from_P_and_S_array_arrivals(events_df, LUTs_dict, array_latlon events_df["lon"] = "" # Calculate various objects only once, for efficiency: - delta_tp_ts_grid = LUTs_dict['trav_times_grid_S'] - LUTs_dict['trav_times_grid_P'] + delta_tp_ts_grid = LUTs_dict["trav_times_grid_S"] - LUTs_dict["trav_times_grid_P"] # Loop over events, processing to find locations: - count=0 + count = 0 for index, row in events_df.iterrows(): - print("Locating event", count+1, '/', len(events_df['bazi1'])) + print("Locating event", count + 1, "/", len(events_df["bazi1"])) # 1. Find effective radius within LUT grid by minimising: # delta_tp_ts - (ts_tt - tp_tt) # To create PDF of uncertainty in grids of LUT for location # Calc. delta_tp_ts: - delta_tp_ts = obspy.UTCDateTime(row['t2']) - obspy.UTCDateTime(row['t1']) + delta_tp_ts = obspy.UTCDateTime(row["t2"]) - obspy.UTCDateTime(row["t1"]) # Find minimum: abs_min_arr = np.abs(delta_tp_ts - delta_tp_ts_grid) tp_ts_res_pdf = 1 - (abs_min_arr / np.max(abs_min_arr)) # 2. Use inclination angle from slowness (of P and/or S) to search from LUT for possible cells: - if row['slow1'] > 0 and row['slow2'] > 0: + if row["slow1"] > 0 and row["slow2"] > 0: # To create PDF for location of cells vertically in LUT # 2.i. For P: # Calculate inclination angle: # v = v_app * sin(inc_angle_from_vert) ?! # Therefore, theta = arcsin( v / v_app ) ?! - v_app_P = 1. / row['slow1'] - inc_angle_P = np.rad2deg(np.arcsin( v_app_P / receiver_vp )) + v_app_P = 1.0 / row["slow1"] + inc_angle_P = np.rad2deg(np.arcsin(v_app_P / receiver_vp)) # And calculate inc. angle pdf: - abs_min_arr = np.abs(LUTs_dict['theta_grid_P'] - inc_angle_P) + abs_min_arr = np.abs(LUTs_dict["theta_grid_P"] - inc_angle_P) P_inc_angle_res_pdf = 1 - (abs_min_arr / np.max(abs_min_arr)) # 2.ii. For S: # Calculate inclination angle: # v = v_app * sin(inc_angle_from_vert) ?! # Therefore, theta = arcsin( v / v_app ) ?! - v_app_S = 1. / row['slow2'] - inc_angle_S = np.rad2deg(np.arcsin( v_app_S / receiver_vs )) + v_app_S = 1.0 / row["slow2"] + inc_angle_S = np.rad2deg(np.arcsin(v_app_S / receiver_vs)) # And calculate inc. angle pdf: - abs_min_arr = np.abs(LUTs_dict['theta_grid_S'] - inc_angle_S) + abs_min_arr = np.abs(LUTs_dict["theta_grid_S"] - inc_angle_S) S_inc_angle_res_pdf = 1 - (abs_min_arr / np.max(abs_min_arr)) # 2.iii. Stack P and S inc. angle PDFs: - PS_stack_inc_angle_res_pdf = (P_inc_angle_res_pdf + S_inc_angle_res_pdf) / 2. + PS_stack_inc_angle_res_pdf = ( + P_inc_angle_res_pdf + S_inc_angle_res_pdf + ) / 2.0 # 3. Combine radius PDF and inc-angle PDF to get best result location within 2D LUT: # Stack pdfs: - stacked_pdf = (tp_ts_res_pdf + PS_stack_inc_angle_res_pdf) / 2. + stacked_pdf = (tp_ts_res_pdf + PS_stack_inc_angle_res_pdf) / 2.0 # And get best result: max_xz_idxs = np.argwhere(stacked_pdf == np.max(stacked_pdf)) x_idx_curr = max_xz_idxs[0][0] z_idx_curr = max_xz_idxs[0][1] - event_x_coord_km = LUTs_dict['vel_model_x_labels'][x_idx_curr] - event_z_coord_km = LUTs_dict['vel_model_z_labels'][z_idx_curr] + event_x_coord_km = LUTs_dict["vel_model_x_labels"][x_idx_curr] + event_z_coord_km = LUTs_dict["vel_model_z_labels"][z_idx_curr] else: event_x_coord_km = np.nan event_z_coord_km = np.nan # Plot workings, if specified: if verbosity > 1: - fig, axes = plt.subplots(nrows=4, figsize=(4,16)) - Z, X = np.meshgrid(LUTs_dict['vel_model_z_labels'], LUTs_dict['vel_model_x_labels']) + fig, axes = plt.subplots(nrows=4, figsize=(4, 16)) + Z, X = np.meshgrid( + LUTs_dict["vel_model_z_labels"], LUTs_dict["vel_model_x_labels"] + ) # Plot P-S pdf: im = axes[0].pcolormesh(X, Z, tp_ts_res_pdf, cmap="Greys_r", vmin=0, vmax=1) plt.colorbar(im, ax=axes[0], label="PDF, $t_{P-S}$") @@ -103,16 +108,20 @@ def locate_events_from_P_and_S_array_arrivals(events_df, LUTs_dict, array_latlon # z_idx_curr = min_xz_idxs[i][1] # axes[0].scatter(LUTs_dict['vel_model_x_labels'][x_idx_curr], LUTs_dict['vel_model_z_labels'][z_idx_curr], c='k') # And plot P inc. angle pdf: - im2 = axes[1].pcolormesh(X, Z, P_inc_angle_res_pdf, cmap="Greys_r", vmin=0, vmax=1) + im2 = axes[1].pcolormesh( + X, Z, P_inc_angle_res_pdf, cmap="Greys_r", vmin=0, vmax=1 + ) plt.colorbar(im2, ax=axes[1], label="PDF, $\\theta_P$") # And plot S inc. angle pdf: - im3 = axes[2].pcolormesh(X, Z, S_inc_angle_res_pdf, cmap="Greys_r", vmin=0, vmax=1) + im3 = axes[2].pcolormesh( + X, Z, S_inc_angle_res_pdf, cmap="Greys_r", vmin=0, vmax=1 + ) plt.colorbar(im3, ax=axes[2], label="PDF, $\\theta_S$") # And plot stacked pdf: im4 = axes[3].pcolormesh(X, Z, stacked_pdf, cmap="Greys_r", vmin=0, vmax=1) plt.colorbar(im4, ax=axes[3], label="stacked PDF") # And plot minimum idx on stacked pdf: - axes[3].scatter(event_x_coord_km, event_z_coord_km, c='g') + axes[3].scatter(event_x_coord_km, event_z_coord_km, c="g") for i in range(len(axes)): axes[i].invert_yaxis() axes[i].set_xlabel("X (m)") @@ -122,28 +131,32 @@ def locate_events_from_P_and_S_array_arrivals(events_df, LUTs_dict, array_latlon # 4. Convert 2D LUT result into 3D cartesian location by combining with bazi.: # (and append to events_df) r_hor_km = event_x_coord_km / 1000 - mean_bazi = (row['bazi1'] + row['bazi2']) / 2 - events_df.iloc[count, events_df.columns.get_loc('x_km')] = r_hor_km * np.sin( np.deg2rad( mean_bazi ) ) - events_df.iloc[count, events_df.columns.get_loc('y_km')] = r_hor_km * np.cos( np.deg2rad( mean_bazi ) ) - events_df.iloc[count, events_df.columns.get_loc('z_km')] = event_z_coord_km / 1000 + mean_bazi = (row["bazi1"] + row["bazi2"]) / 2 + events_df.iloc[count, events_df.columns.get_loc("x_km")] = r_hor_km * np.sin( + np.deg2rad(mean_bazi) + ) + events_df.iloc[count, events_df.columns.get_loc("y_km")] = r_hor_km * np.cos( + np.deg2rad(mean_bazi) + ) + events_df.iloc[count, events_df.columns.get_loc("z_km")] = ( + event_z_coord_km / 1000 + ) # Calculate event lat and lons relative to array centre: - events_df.iloc[count, events_df.columns.get_loc('lat')] = array_latlon[0] + obspy.geodetics.base.kilometers2degrees(events_df.iloc[count, events_df.columns.get_loc('y_km')]) - events_df.iloc[count, events_df.columns.get_loc('lon')] = array_latlon[1] + obspy.geodetics.base.kilometers2degrees(events_df.iloc[count, events_df.columns.get_loc('x_km')]) + events_df.iloc[count, events_df.columns.get_loc("lat")] = array_latlon[ + 0 + ] + obspy.geodetics.base.kilometers2degrees( + events_df.iloc[count, events_df.columns.get_loc("y_km")] + ) + events_df.iloc[count, events_df.columns.get_loc("lon")] = array_latlon[ + 1 + ] + obspy.geodetics.base.kilometers2degrees( + events_df.iloc[count, events_df.columns.get_loc("x_km")] + ) # Update event count: - count+=1 + count += 1 return events_df - - - - - - - -#----------------------------------------------- End: Define main functions ----------------------------------------------- - - - +# ----------------------------------------------- End: Define main functions ----------------------------------------------- diff --git a/SeisSeeker/processing/lookup_table_manager.py b/SeisSeeker/processing/lookup_table_manager.py index 34b3747..ce967ef 100755 --- a/SeisSeeker/processing/lookup_table_manager.py +++ b/SeisSeeker/processing/lookup_table_manager.py @@ -1,5 +1,5 @@ #!/Users/eart0504/opt/anaconda3/bin/python -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Script Description: # Script to calculate travel times lookup tables for various seismic phases. @@ -10,24 +10,26 @@ # Created by Tom Hudson, 17th August 2022 -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Import neccessary modules: import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -import skfmm # For fast-marching travel-time lookup tables +import skfmm # For fast-marching travel-time lookup tables + # import ttcrpy.rgrid as ttcrpy_rgrid # For ray-tracing based incidence angles -import gc -#import pykonal # For ray-tracing based incidence angles +import gc + +# import pykonal # For ray-tracing based incidence angles -#----------------------------------------------- Define constants and parameters ----------------------------------------------- +# ----------------------------------------------- Define constants and parameters ----------------------------------------------- -#----------------------------------------------- End: Define constants and parameters ----------------------------------------------- +# ----------------------------------------------- End: Define constants and parameters ----------------------------------------------- -#----------------------------------------------- Define main functions ----------------------------------------------- -def read_1D_vel_model_to_3D_model_for_fmm(oneD_vel_model_z_df, extent_xy_m=[3000,3000], dxyz=[1.,1.,1.]): +# ----------------------------------------------- Define main functions ----------------------------------------------- +def read_1D_vel_model_to_3D_model_for_fmm( + oneD_vel_model_z_df, extent_xy_m=[3000, 3000], dxyz=[1.0, 1.0, 1.0] +): """Function to create specific velocity model. Inputs: oneD_vel_model_z_df - Pandas DataFrame of shape (z_extent, 3) with the columns corresponding to: depth, vp, vs @@ -37,12 +39,12 @@ def read_1D_vel_model_to_3D_model_for_fmm(oneD_vel_model_z_df, extent_xy_m=[3000 vel_model_arr_S - 3D Velocity grid for S wave ix, iy, iz - Grids containing index labels """ - dx = dxyz[0] # Grid spacing in x dir - dy = dxyz[1] # Grid spacing in x dir - dz = dxyz[2] # Grid spacing in z dir - vel_model_x_labels = np.arange(0., extent_xy_m[0]+dx, dx) - vel_model_y_labels = np.arange(0., extent_xy_m[1]+dy, dy) - vel_model_z_labels = oneD_vel_model_z_df['depth'].values + dx = dxyz[0] # Grid spacing in x dir + dy = dxyz[1] # Grid spacing in x dir + dz = dxyz[2] # Grid spacing in z dir + vel_model_x_labels = np.arange(0.0, extent_xy_m[0] + dx, dx) + vel_model_y_labels = np.arange(0.0, extent_xy_m[1] + dy, dy) + vel_model_z_labels = oneD_vel_model_z_df["depth"].values # Create the index grids: iy, ix, iz = np.meshgrid(vel_model_y_labels, vel_model_x_labels, vel_model_z_labels) # Create the P and S wave model: @@ -50,9 +52,15 @@ def read_1D_vel_model_to_3D_model_for_fmm(oneD_vel_model_z_df, extent_xy_m=[3000 vel_model_arr_S = np.zeros(np.shape(ix), dtype=float) for i in range(vel_model_arr_P.shape[0]): for j in range(vel_model_arr_P.shape[1]): - vel_model_arr_P[i,j,:] = oneD_vel_model_z_df['vp'] - vel_model_arr_S[i,j,:] = oneD_vel_model_z_df['vs'] - return vel_model_arr_P, vel_model_arr_S, vel_model_x_labels, vel_model_y_labels, vel_model_z_labels + vel_model_arr_P[i, j, :] = oneD_vel_model_z_df["vp"] + vel_model_arr_S[i, j, :] = oneD_vel_model_z_df["vs"] + return ( + vel_model_arr_P, + vel_model_arr_S, + vel_model_x_labels, + vel_model_y_labels, + vel_model_z_labels, + ) def vec_norm(x): @@ -60,7 +68,9 @@ def vec_norm(x): return np.sqrt(x.dot(x)) -def read_1D_vel_model_to_2D_model_for_fmm(oneD_vel_model_z_df, extent_x_m=3000, dxz=[1.,1.]): +def read_1D_vel_model_to_2D_model_for_fmm( + oneD_vel_model_z_df, extent_x_m=3000, dxz=[1.0, 1.0] +): """Function to create specific velocity model in 2D (x, z). Inputs: oneD_vel_model_z_df - Pandas DataFrame of shape (z_extent, 3) with the columns corresponding to: depth, vp, vs @@ -69,26 +79,26 @@ def read_1D_vel_model_to_2D_model_for_fmm(oneD_vel_model_z_df, extent_x_m=3000, vel_model_arr_P - 3D Velocity grid for P wave vel_model_arr_S - 3D Velocity grid for S wave """ - dx = dxz[0] # Grid spacing in x dir - dz = dxz[1] # Grid spacing in z dir - vel_model_x_labels = np.arange(0., extent_x_m+dx, dx) - vel_model_z_labels = oneD_vel_model_z_df['depth'].values + dx = dxz[0] # Grid spacing in x dir + dz = dxz[1] # Grid spacing in z dir + vel_model_x_labels = np.arange(0.0, extent_x_m + dx, dx) + vel_model_z_labels = oneD_vel_model_z_df["depth"].values # Create the index grids: iz, ix = np.meshgrid(vel_model_z_labels, vel_model_x_labels) # Create the P and S wave model: vel_model_arr_P = np.zeros(np.shape(ix), dtype=float) vel_model_arr_S = np.zeros(np.shape(iz), dtype=float) for i in range(vel_model_arr_P.shape[0]): - vel_model_arr_P[i,:] = oneD_vel_model_z_df['vp'] - vel_model_arr_S[i,:] = oneD_vel_model_z_df['vs'] + vel_model_arr_P[i, :] = oneD_vel_model_z_df["vp"] + vel_model_arr_S[i, :] = oneD_vel_model_z_df["vs"] return vel_model_arr_P, vel_model_arr_S, vel_model_x_labels, vel_model_z_labels -def ray_tracer(v_model, dxyz, src_loc=[0,0,0], rx_loc=[0,0,0]): +def ray_tracer(v_model, dxyz, src_loc=[0, 0, 0], rx_loc=[0, 0, 0]): """Function to perform ray-tracing, using pykonal package. All units are in km. src_loc, rx_loc are in km x,y,z.""" # Define the solver: - solver = pykonal.solver.PointSourceSolver(coord_sys="cartesian") + solver = pykonal.solver.PointSourceSolver(coord_sys="cartesian") # Define the computational domain: solver.vv.min_coords = 0, 0, 0 solver.vv.node_intervals = dxyz[0], dxyz[1], dxyz[2] @@ -111,19 +121,24 @@ def find_nearest(array, value): idx = (np.abs(array - value)).argmin() return array[idx], idx -def create_2D_LUT(oneD_vel_model_z_df, array_centre_xz, extent_x_m=3000., dxz=[1.,1.], n_threads=1): - """Get theoretical P and S travel times for all nodes in a lookup table, relative to the centre of the array + +def create_2D_LUT( + oneD_vel_model_z_df, array_centre_xz, extent_x_m=3000.0, dxz=[1.0, 1.0], n_threads=1 +): + """Get theoretical P and S travel times for all nodes in a lookup table, relative to the centre of the array (using Eikonal method). Inputs: oneD_vel_model_z_df - Pandas DataFrame of shape (z_extent, 3) with the columns corresponding to: depth, vp, vs. array_centre_xz - The location of the array centre within the proposed lookup table grid, in metres. """ # Get 2D velocity model: - dx = dxz[0] # Grid spacing in x dir - dz = dxz[1] # Grid spacing in z dir - vel_model_arr_P, vel_model_arr_S, vel_model_x_labels, vel_model_z_labels = read_1D_vel_model_to_2D_model_for_fmm(oneD_vel_model_z_df, - extent_x_m=extent_x_m, - dxz=dxz) + dx = dxz[0] # Grid spacing in x dir + dz = dxz[1] # Grid spacing in z dir + vel_model_arr_P, vel_model_arr_S, vel_model_x_labels, vel_model_z_labels = ( + read_1D_vel_model_to_2D_model_for_fmm( + oneD_vel_model_z_df, extent_x_m=extent_x_m, dxz=dxz + ) + ) # Create travel-time lookup table grids: # (from fast marching method) @@ -133,10 +148,10 @@ def create_2D_LUT(oneD_vel_model_z_df, array_centre_xz, extent_x_m=3000., dxz=[1 val, x_idx = find_nearest(vel_model_x_labels, array_centre_xz[0]) val, z_idx = find_nearest(vel_model_z_labels, array_centre_xz[1]) # Set array_centre_xyz location: - phi[x_idx, z_idx] = 1. + phi[x_idx, z_idx] = 1.0 # Calculate travel times array to all the points in the grid: - trav_times_grid_P = skfmm.travel_time(phi,vel_model_arr_P,dx=dxz) - trav_times_grid_S = skfmm.travel_time(phi,vel_model_arr_S,dx=dxz) + trav_times_grid_P = skfmm.travel_time(phi, vel_model_arr_P, dx=dxz) + trav_times_grid_S = skfmm.travel_time(phi, vel_model_arr_S, dx=dxz) # And create incidence angle lookup table grids: # (from ray tracing) @@ -145,8 +160,12 @@ def create_2D_LUT(oneD_vel_model_z_df, array_centre_xz, extent_x_m=3000., dxz=[1 # z_node_labels = vel_model_z_labels # rgrid = ttcrpy_rgrid.Grid2d(x_node_labels, z_node_labels, cell_slowness=False, n_threads=n_threads) # Specify velocity model for ray tracing: - vel_model_arr_P_3D = vel_model_arr_P.reshape(1, vel_model_arr_P.shape[0], vel_model_arr_P.shape[1]) - vel_model_arr_S_3D = vel_model_arr_S.reshape(1, vel_model_arr_S.shape[0], vel_model_arr_S.shape[1]) + vel_model_arr_P_3D = vel_model_arr_P.reshape( + 1, vel_model_arr_P.shape[0], vel_model_arr_P.shape[1] + ) + vel_model_arr_S_3D = vel_model_arr_S.reshape( + 1, vel_model_arr_S.shape[0], vel_model_arr_S.shape[1] + ) # Calculate rays for each point in the grid: theta_grid_P = np.zeros(trav_times_grid_P.shape) theta_grid_S = np.zeros(trav_times_grid_P.shape) @@ -157,96 +176,99 @@ def create_2D_LUT(oneD_vel_model_z_df, array_centre_xz, extent_x_m=3000., dxz=[1 for j in range(theta_grid_P.shape[1]): # Print progress: if count % 100 == 0: - print("Processing for ray", count, "/", theta_grid_P.shape[0]*theta_grid_P.shape[1]) + print( + "Processing for ray", + count, + "/", + theta_grid_P.shape[0] * theta_grid_P.shape[1], + ) count += 1 # Calculate ray-tracing and incidence angle for P wave: # Calculate rays for current event: # (Note that coords are converted into 3D and km) - node_coords = np.array([0, vel_model_x_labels[i], vel_model_z_labels[j]], dtype=float) / 1000 - node_coords = node_coords + 0.001 # Add 1 metre, so that node coords are non-zero - receiver_coords = np.array([0, array_centre_xz[0], array_centre_xz[1]], dtype=float) / 1000 + node_coords = ( + np.array([0, vel_model_x_labels[i], vel_model_z_labels[j]], dtype=float) + / 1000 + ) + node_coords = ( + node_coords + 0.001 + ) # Add 1 metre, so that node coords are non-zero + receiver_coords = ( + np.array([0, array_centre_xz[0], array_centre_xz[1]], dtype=float) + / 1000 + ) # tt, rays = rgrid.raytrace(event_coords, receiver_coords, 1./vel_model_arr_P, return_rays=True) # Perform ray tracing: dxyz = np.array([dx, dx, dz], dtype=float) / 1000 - ray = ray_tracer(vel_model_arr_P_3D / 1000, dxyz, src_loc=node_coords, rx_loc=receiver_coords) + ray = ray_tracer( + vel_model_arr_P_3D / 1000, + dxyz, + src_loc=node_coords, + rx_loc=receiver_coords, + ) # Calculate incidence angle at array: try: - ray_vec_at_receiver = - np.array([ ray[-1,1] - ray[-2,1], ray[-1,2] - ray[-2,2] ]) # Note minus sign, as defining as vector out from array - vert_vec = np.array([ 0, 1]) - theta_curr = np.arccos( ( ray_vec_at_receiver.dot(vert_vec) ) / ( vec_norm(ray_vec_at_receiver) * vec_norm(vert_vec) ) ) # cos(theta) = a.b / |a| |b| + ray_vec_at_receiver = -np.array( + [ray[-1, 1] - ray[-2, 1], ray[-1, 2] - ray[-2, 2]] + ) # Note minus sign, as defining as vector out from array + vert_vec = np.array([0, 1]) + theta_curr = np.arccos( + (ray_vec_at_receiver.dot(vert_vec)) + / (vec_norm(ray_vec_at_receiver) * vec_norm(vert_vec)) + ) # cos(theta) = a.b / |a| |b| theta_curr = np.rad2deg(theta_curr) - theta_grid_P[i,j] = theta_curr + theta_grid_P[i, j] = theta_curr except IndexError: # Or assign previous value, if failed to trace rays for some reason: - if j>0: - theta_grid_P[i,j] = theta_grid_P[i,j-1] + if j > 0: + theta_grid_P[i, j] = theta_grid_P[i, j - 1] else: - theta_grid_P[i,j] = theta_grid_P[i-1,j] + theta_grid_P[i, j] = theta_grid_P[i - 1, j] # And clear up: - del ray + del ray gc.collect() # Calculate ray-tracing and incidence angle for S wave: # Calculate rays for current event: # (Uses some parameters as for P wave) # Perform ray tracing: - ray = ray_tracer(vel_model_arr_S_3D / 1000, dxyz, src_loc=node_coords, rx_loc=receiver_coords) + ray = ray_tracer( + vel_model_arr_S_3D / 1000, + dxyz, + src_loc=node_coords, + rx_loc=receiver_coords, + ) # Calculate incidence angle at array: try: - ray_vec_at_receiver = - np.array([ ray[-1,1] - ray[-2,1], ray[-1,2] - ray[-2,2] ]) # Note minus sign, as defining as vector out from array - vert_vec = np.array([ 0, 1]) - theta_curr = np.arccos( ( ray_vec_at_receiver.dot(vert_vec) ) / ( vec_norm(ray_vec_at_receiver) * vec_norm(vert_vec) ) ) # cos(theta) = a.b / |a| |b| + ray_vec_at_receiver = -np.array( + [ray[-1, 1] - ray[-2, 1], ray[-1, 2] - ray[-2, 2]] + ) # Note minus sign, as defining as vector out from array + vert_vec = np.array([0, 1]) + theta_curr = np.arccos( + (ray_vec_at_receiver.dot(vert_vec)) + / (vec_norm(ray_vec_at_receiver) * vec_norm(vert_vec)) + ) # cos(theta) = a.b / |a| |b| theta_curr = np.rad2deg(theta_curr) - theta_grid_S[i,j] = theta_curr + theta_grid_S[i, j] = theta_curr except IndexError: # Or assign previous value, if failed to trace rays for some reason: - if j>0: - theta_grid_S[i,j] = theta_grid_S[i,j-1] + if j > 0: + theta_grid_S[i, j] = theta_grid_S[i, j - 1] else: - theta_grid_S[i,j] = theta_grid_S[i-1,j] + theta_grid_S[i, j] = theta_grid_S[i - 1, j] # And clear up: - del ray + del ray gc.collect() - - - - return trav_times_grid_P, trav_times_grid_S, theta_grid_P, theta_grid_S, vel_model_x_labels, vel_model_z_labels - - - - - - -#----------------------------------------------- End: Define main functions ----------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - + return ( + trav_times_grid_P, + trav_times_grid_S, + theta_grid_P, + theta_grid_S, + vel_model_x_labels, + vel_model_z_labels, + ) +# ----------------------------------------------- End: Define main functions ----------------------------------------------- diff --git a/SeisSeeker/processing/lookup_table_manager_3D_backup.py b/SeisSeeker/processing/lookup_table_manager_3D_backup.py index d199e18..c7d0abc 100755 --- a/SeisSeeker/processing/lookup_table_manager_3D_backup.py +++ b/SeisSeeker/processing/lookup_table_manager_3D_backup.py @@ -1,5 +1,5 @@ #!/Users/eart0504/opt/anaconda3/bin/python -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Script Description: # Script to calculate travel times lookup tables for various seismic phases. @@ -10,21 +10,23 @@ # Created by Tom Hudson, 17th August 2022 -#----------------------------------------------------------------------------------------------------------------------------------------- +# ----------------------------------------------------------------------------------------------------------------------------------------- # Import neccessary modules: import numpy as np -import pandas as pd +import pandas as pd import matplotlib.pyplot as plt -import skfmm +import skfmm -#----------------------------------------------- Define constants and parameters ----------------------------------------------- +# ----------------------------------------------- Define constants and parameters ----------------------------------------------- -#----------------------------------------------- End: Define constants and parameters ----------------------------------------------- +# ----------------------------------------------- End: Define constants and parameters ----------------------------------------------- -#----------------------------------------------- Define main functions ----------------------------------------------- -def read_1D_vel_model_for_fmm(oneD_vel_model_z_df, extent_xy_m=[3000,3000], dxyz=[1.,1.,1.]): +# ----------------------------------------------- Define main functions ----------------------------------------------- +def read_1D_vel_model_for_fmm( + oneD_vel_model_z_df, extent_xy_m=[3000, 3000], dxyz=[1.0, 1.0, 1.0] +): """Function to create specific ice velocity model. Inputs: oneD_vel_model_z_df - Pandas DataFrame of shape (z_extent, 3) with the columns corresponding to: depth, vp, vs @@ -34,12 +36,12 @@ def read_1D_vel_model_for_fmm(oneD_vel_model_z_df, extent_xy_m=[3000,3000], dxyz vel_model_arr_S - 3D Velocity grid for S wave ix, iy, iz - Grids containing index labels """ - dx = dxyz[0] # Grid spacing in x dir - dy = dxyz[1] # Grid spacing in x dir - dz = dxyz[2] # Grid spacing in z dir - vel_model_x_labels = np.arange(0., extent_xy_m[0]+dx, dx) - vel_model_y_labels = np.arange(0., extent_xy_m[1]+dy, dy) - vel_model_z_labels = oneD_vel_model_z_df['depth'].values + dx = dxyz[0] # Grid spacing in x dir + dy = dxyz[1] # Grid spacing in x dir + dz = dxyz[2] # Grid spacing in z dir + vel_model_x_labels = np.arange(0.0, extent_xy_m[0] + dx, dx) + vel_model_y_labels = np.arange(0.0, extent_xy_m[1] + dy, dy) + vel_model_z_labels = oneD_vel_model_z_df["depth"].values # Create the index grids: iy, ix, iz = np.meshgrid(vel_model_y_labels, vel_model_x_labels, vel_model_z_labels) # Create the P and S wave model: @@ -47,17 +49,30 @@ def read_1D_vel_model_for_fmm(oneD_vel_model_z_df, extent_xy_m=[3000,3000], dxyz vel_model_arr_S = np.zeros(np.shape(ix), dtype=float) for i in range(vel_model_arr_P.shape[0]): for j in range(vel_model_arr_P.shape[1]): - vel_model_arr_P[i,j,:] = oneD_vel_model_z_df['vp'] - vel_model_arr_S[i,j,:] = oneD_vel_model_z_df['vs'] - return vel_model_arr_P, vel_model_arr_S, vel_model_x_labels, vel_model_y_labels, vel_model_z_labels + vel_model_arr_P[i, j, :] = oneD_vel_model_z_df["vp"] + vel_model_arr_S[i, j, :] = oneD_vel_model_z_df["vs"] + return ( + vel_model_arr_P, + vel_model_arr_S, + vel_model_x_labels, + vel_model_y_labels, + vel_model_z_labels, + ) + def find_nearest(array, value): array = np.asarray(array) idx = (np.abs(array - value)).argmin() return array[idx], idx -def create_LUT(oneD_vel_model_z_df, array_centre_xyz, extent_xy_m=[3000.,3000.], dxyz=[1.,1.,1.]): - """Get theoretical P and S travel times for all nodes in a lookup table, relative to the centre of the array + +def create_LUT( + oneD_vel_model_z_df, + array_centre_xyz, + extent_xy_m=[3000.0, 3000.0], + dxyz=[1.0, 1.0, 1.0], +): + """Get theoretical P and S travel times for all nodes in a lookup table, relative to the centre of the array (using Eikonal method). Inputs: oneD_vel_model_z_df - Pandas DataFrame of shape (z_extent, 3) with the columns corresponding to: depth, vp, vs. @@ -65,13 +80,19 @@ def create_LUT(oneD_vel_model_z_df, array_centre_xyz, extent_xy_m=[3000.,3000.], src_depth_xyz - The source depth in x,y,z in metres. """ # Get velocity model: - dx = dxyz[0] # Grid spacing in x dir - dy = dxyz[1] # Grid spacing in x dir - dz = dxyz[2] # Grid spacing in z dir - vel_model_arr_P, vel_model_arr_S, vel_model_x_labels, vel_model_y_labels, vel_model_z_labels = read_1D_vel_model_for_fmm(oneD_vel_model_z_df, - extent_xy_m=extent_xy_m, - dxyz=dxyz) - + dx = dxyz[0] # Grid spacing in x dir + dy = dxyz[1] # Grid spacing in x dir + dz = dxyz[2] # Grid spacing in z dir + ( + vel_model_arr_P, + vel_model_arr_S, + vel_model_x_labels, + vel_model_y_labels, + vel_model_z_labels, + ) = read_1D_vel_model_for_fmm( + oneD_vel_model_z_df, extent_xy_m=extent_xy_m, dxyz=dxyz + ) + # Calculate travel times for all points in the grid: # Setup array for masking/showing where array centre is is: phi = -np.ones(vel_model_arr_P.shape) @@ -79,35 +100,43 @@ def create_LUT(oneD_vel_model_z_df, array_centre_xyz, extent_xy_m=[3000.,3000.], val, y_idx = find_nearest(vel_model_y_labels, array_centre_xyz[1]) val, z_idx = find_nearest(vel_model_z_labels, array_centre_xyz[2]) # Set array_centre_xyz location: - phi[x_idx, y_idx, z_idx] = 1. + phi[x_idx, y_idx, z_idx] = 1.0 # Calculate travel times array to all the points in the grid: - trav_times_grid_P = skfmm.travel_time(phi,vel_model_arr_P,dx=dxyz) - trav_times_grid_S = skfmm.travel_time(phi,vel_model_arr_S,dx=dxyz) - - return trav_times_grid_P, trav_times_grid_S - - - + trav_times_grid_P = skfmm.travel_time(phi, vel_model_arr_P, dx=dxyz) + trav_times_grid_S = skfmm.travel_time(phi, vel_model_arr_S, dx=dxyz) + return trav_times_grid_P, trav_times_grid_S -#----------------------------------------------- End: Define main functions ----------------------------------------------- +# ----------------------------------------------- End: Define main functions ----------------------------------------------- -#----------------------------------------------- Run script ----------------------------------------------- +# ----------------------------------------------- Run script ----------------------------------------------- if __name__ == "__main__": # Example of usage: # (For a 2200 x 1 x 2100 m grid with receivers in a line from the surface to 2 km then along horizontally for 1 km) # Create velocity model: - oneD_vel_model_z_df = pd.DataFrame({'depth': np.arange(0,3000,1.), 'vp': 3500*np.ones(2100), 'vs': 2000*np.ones(2100)}) - station_xs = np.concatenate((1100*np.ones(len(np.arange(1,2000))), np.arange(100,1100)[::-1])) + oneD_vel_model_z_df = pd.DataFrame( + { + "depth": np.arange(0, 3000, 1.0), + "vp": 3500 * np.ones(2100), + "vs": 2000 * np.ones(2100), + } + ) + station_xs = np.concatenate( + (1100 * np.ones(len(np.arange(1, 2000))), np.arange(100, 1100)[::-1]) + ) station_ys = np.zeros(len(station_xs)) - station_zs = np.concatenate((np.arange(1,2000), 2000*np.ones(1000))) - station_xyz_coords_df = pd.DataFrame({'x_m': station_xs, 'y_m': station_ys, 'z_m': station_zs}) - src_xyz = [600., 0., 1500.] + station_zs = np.concatenate((np.arange(1, 2000), 2000 * np.ones(1000))) + station_xyz_coords_df = pd.DataFrame( + {"x_m": station_xs, "y_m": station_ys, "z_m": station_zs} + ) + src_xyz = [600.0, 0.0, 1500.0] # And get travel times: - trav_times_P, trav_times_S = calc_travel_times_fmm(oneD_vel_model_z_df, station_xyz_coords_df, src_xyz, extent_xy_m=[2200.,1.]) + trav_times_P, trav_times_S = calc_travel_times_fmm( + oneD_vel_model_z_df, station_xyz_coords_df, src_xyz, extent_xy_m=[2200.0, 1.0] + ) # And plot result: plt.figure() @@ -116,32 +145,3 @@ def create_LUT(oneD_vel_model_z_df, array_centre_xyz, extent_xy_m=[3000.,3000.], plt.show() print("Finished") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From ca9b10f286aeb951c2de929c03bd22932d234ae4 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 17 Sep 2025 10:48:36 +0100 Subject: [PATCH 087/103] made some flake8 fixes --- SeisSeeker/processing/detection.py | 53 ++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index f94c78c..369a467 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -29,13 +29,13 @@ logger = logging.getLogger(__name__) -# ----------------------------------------------- Define main functions ----------------------------------------------- +# ---------- Define main functions ------------ class CustomError(Exception): pass -def flatten_list(l): - return [item for sublist in l for item in sublist] +def flatten_list(list_to_flatten): + return [item for sublist in list_to_flatten for item in sublist] def xy_to_rtheta(x, y): @@ -67,11 +67,45 @@ def _fast_freq_domain_array_proc( n_t_samp, remove_autocorr, ): - """Function to perform array processing fast due to being designed to - be wrapped using Numba. Function inspired by Bowden et al. (2021). + """ + Performs array processing using methodinspired by Bowden et al. (2021). Performs array processing in polar coordinates. + Designed to be wrapped using Numba to improve performance. + Parameters: + ---------- + data : np.ndarray + 3D numpy array of data to process. Shape must be (n_windows, n_stations, n_t_samp). + min_sl : float + Minimum slowness to analyse for, in s/km. + max_sl : float + Maximum slowness to analyse for, in s/km. + n_sl : int + Number of slowness values to analyse between min_sl and max_sl. + min_baz : float + Minimum back-azimuth, in degrees. + max_baz : float + Maximum back-azimuth, in degrees. + n_baz : int + Number of back-azimuth values to analyse between min_baz and max_baz. + fs : float + Sampling frequency of the data, in Hz. + target_freqs : list + List of target frequencies to analyse, in Hz. + xx : np.ndarray + 2D numpy array of x-coordinates of station locations, in km. + yy : np.ndarray + 2D numpy array of y-coordinates of station locations, in km. + n_stations : int + Number of stations in the array. + n_t_samp : int + Number of time samples in the data. + remove_autocorr : bool + Whether to remove autocorrelations from the data. + Returns: - Pfreq_all + ---------- + Pfreq_all : np.ndarray + 4D numpy array of processed data. Shape will be (n_windows, len(target_freqs), n_sl, n_baz). """ # Define grid of slownesses: # number of pixes in x and y @@ -79,8 +113,6 @@ def _fast_freq_domain_array_proc( ur = np.linspace(min_sl, max_sl, n_sl) utheta = np.linspace(min_baz, max_baz, n_baz) utheta_rad = np.deg2rad(utheta) - dur = ur[1] - ur[0] - dutheta = utheta[1] - utheta[0] # Compute time-shifts once: # (so that don't have to do it for every frequency) @@ -1167,8 +1199,9 @@ def _beamforming(self, st_trimmed, verbosity=0): def _calculate_mad(self, x, scale=1.4826): """ Calculates the Median Absolute Deviation (MAD) of the input array x. - Outputs an array of scaled mean absolute deviation values for the input array, x, - scaled to provide an estimation of the standard deviation of the distribution. + Outputs an array of scaled mean absolute deviation values for the + input array, x, scaled to provide an estimation of the standard + deviation of the distribution. """ # Calculate median and mad values: mad = np.median(np.abs(x - np.median(x))) From 84700f21f7e343ab8b4fb2130d6ef51e4fef611c Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 17 Sep 2025 14:19:21 +0100 Subject: [PATCH 088/103] vectorised calculated tlib and made the MAD scale a global variable --- SeisSeeker/processing/detection.py | 115 +++++++++++++---------------- 1 file changed, 50 insertions(+), 65 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 369a467..7d5aa21 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -28,6 +28,9 @@ logger = logging.getLogger(__name__) +# GLobal constants: +MAD_SCALE = 1.4826 # Scale factor to convert MAD to std. dev. + # ---------- Define main functions ------------ class CustomError(Exception): @@ -50,7 +53,7 @@ def xy_to_rtheta(x, y): return r, theta -@jit(nopython=True, parallel=True) # , nogil=True) +@jit(nopython=True, parallel=True) def _fast_freq_domain_array_proc( data, min_sl, @@ -68,13 +71,15 @@ def _fast_freq_domain_array_proc( remove_autocorr, ): """ - Performs array processing using methodinspired by Bowden et al. (2021). + Performs array processing using method inspired by Bowden et al. (2021). Performs array processing in polar coordinates. Designed to be wrapped using Numba to improve performance. + Parameters: ---------- data : np.ndarray - 3D numpy array of data to process. Shape must be (n_windows, n_stations, n_t_samp). + 3D numpy array of data to process. Shape must be + (n_windows, n_stations, n_t_samp). min_sl : float Minimum slowness to analyse for, in s/km. max_sl : float @@ -92,9 +97,9 @@ def _fast_freq_domain_array_proc( target_freqs : list List of target frequencies to analyse, in Hz. xx : np.ndarray - 2D numpy array of x-coordinates of station locations, in km. + 1D numpy array of x-coordinates of station locations, in km. yy : np.ndarray - 2D numpy array of y-coordinates of station locations, in km. + 1D numpy array of y-coordinates of station locations, in km. n_stations : int Number of stations in the array. n_t_samp : int @@ -105,68 +110,60 @@ def _fast_freq_domain_array_proc( Returns: ---------- Pfreq_all : np.ndarray - 4D numpy array of processed data. Shape will be (n_windows, len(target_freqs), n_sl, n_baz). + 4D numpy array of processed data. Shape will be + (n_windows, len(target_freqs), n_sl, n_baz). """ # Define grid of slownesses: - # number of pixes in x and y - # (Determines number of phase shifts to perform) ur = np.linspace(min_sl, max_sl, n_sl) utheta = np.linspace(min_baz, max_baz, n_baz) utheta_rad = np.deg2rad(utheta) - # Compute time-shifts once: - # (so that don't have to do it for every frequency) - tlib = np.zeros((n_stations, n_sl, n_baz), dtype=np.complex128) - for ir in range(0, n_sl): - for itheta in range(0, n_baz): - # tlib[:,ix,iy] = xx*ux[ix] + yy*uy[iy] # (distance x slowness = distance / velocity = time) - tlib[:, ir, itheta] = xx * ur[ir] * np.sin((utheta_rad[itheta])) + yy * ur[ - ir - ] * np.cos( - (utheta_rad[itheta]) - ) # (distance x slowness = distance / velocity = time) - # Since receivers are relative to the array centre, can shift all receivers back to that centre. - - # Create data stores: + # # Compute time-shifts once: + # tlib = np.zeros((n_stations, n_sl, n_baz), dtype=np.complex128) + # # r, theta as this is polar coord system: + # for ir in range(n_sl): + # for itheta in range(n_baz): + # tlib[:, ir, itheta] = xx * ur[ir] * np.sin(utheta_rad[itheta]) + yy * ur[ + # ir + # ] * np.cos(utheta_rad[itheta]) + + # Vectorized computation of time-shifts for all stations, slowness, and back-azimuth + # xx and yy are (n_stations,) arrays, ur is (n_sl,), utheta_rad is (n_baz,) + # We want tlib shape (n_stations, n_sl, n_baz) + xx_ = xx[:, np.newaxis, np.newaxis] # shape (n_stations, 1, 1) + yy_ = yy[:, np.newaxis, np.newaxis] # shape (n_stations, 1, 1) + ur_ = ur[np.newaxis, :, np.newaxis] # shape (1, n_sl, 1) + utheta_rad_ = utheta_rad[np.newaxis, np.newaxis, :] # shape (1, 1, n_baz) + tlib_tmp = xx_ * ur_ * np.sin(utheta_rad_) + yy_ * ur_ * np.cos(utheta_rad_) + # recast array as complex128 + tlib = tlib_tmp.astype(np.complex128) + del tlib_tmp, xx_, yy_, ur_, utheta_rad_ + gc.collect() + Pfreq_all = np.zeros( (data.shape[0], len(target_freqs), n_sl, n_baz), dtype=np.complex128 - ) # Explicitly create Pxx_all, as otherwise prange won't work correctly. - - # Then loop over windows: + ) for win_idx in prange(data.shape[0]): - # Calculate spectra: - # Construct data structure: - nfft = 2.0 ** np.ceil(np.log2(n_t_samp)) - nfft = np.array(nfft, dtype=np.int64) - Pxx_all = np.zeros( - (np.int64((nfft / 2) + 1), n_stations), dtype=np.complex128 - ) # Power spectra + nfft = int(2 ** np.ceil(np.log2(n_t_samp))) + Pxx_all = np.zeros((int(nfft / 2) + 1, n_stations), dtype=np.complex128) dt = 1.0 / fs - df = 1.0 / (2.0 * nfft * dt) - xf = np.linspace(0.0, 1.0 / (2.0 * dt), np.int64((nfft / 2) + 1)) + xf = np.linspace(0.0, 1.0 / (2.0 * dt), int(nfft / 2) + 1) # Calculate power spectra for all stations: for sta_idx in range(n_stations): - # Calculate spectra for current station: - ###Pxx_all[:,sta_idx] = np.fft.rfft(data[win_idx,sta_idx,:], n=nfft) # (Use real fft, as input data is real) # DOESN'T WORK WITH NUMBA! with objmode(Pxx_curr="complex128[:]"): Pxx_curr = np.fft.rfft(data[win_idx, sta_idx, :], n=nfft) Pxx_all[:, sta_idx] = Pxx_curr - # Loop over all freqs, performing phase shifts: Pfreq = np.zeros((len(target_freqs), n_sl, n_baz), dtype=np.complex128) - counter_grid = 0 for ii in range(len(target_freqs)): - # Find closest current freq.: target_f = target_freqs[ii] curr_f_idx = (np.abs(xf - target_f)).argmin() - # Construct a matrix of each station-station correlation before any phase shifts Rxx = np.zeros((n_stations, n_stations), dtype=np.complex128) - for i1 in range(0, n_stations): - for i2 in range(0, n_stations): - # Remove autocorrelations: + for i1 in range(n_stations): + for i2 in range(n_stations): if remove_autocorr: - if not i1 == i2: + if i1 != i2: Rxx[i1, i2] = ( np.conj(Pxx_all[curr_f_idx, i1]) * Pxx_all[curr_f_idx, i2] @@ -178,24 +175,13 @@ def _fast_freq_domain_array_proc( np.conj(Pxx_all[curr_f_idx, i1]) * Pxx_all[curr_f_idx, i2] ) - # And loop over phase shifts, calculating cross-correlation power: - for ir in range(0, n_sl): - for itheta in range(0, n_baz): - timeshifts = tlib[ - :, ir, itheta - ] # Calculate the "steering vector" (a vector in frequency space, based on phase-shift) - a = np.exp( - -1j * 2 * np.pi * target_f * timeshifts - ) # (a is a steering vector, to allign all traces with array centre) + for ir in range(n_sl): + for itheta in range(n_baz): + timeshifts = tlib[:, ir, itheta] + a = np.exp(-1j * 2 * np.pi * target_f * timeshifts) aconj = np.conj(a) - Pfreq[ii, ir, itheta] = np.dot( - np.dot(aconj, Rxx), a - ) # Cross-correlation, with two timeshifts applied to push the two stations to the centre point. - # np.dot is returning a sum product here making this - # effectively eqn 7 of Ruigrok et al., (2017) - # (This can also be seen as projecting Rxx onto a new basis.) - - # And append output to datastore: + Pfreq[ii, ir, itheta] = np.dot(np.dot(aconj, Rxx), a) + Pfreq_all[win_idx, :, :, :] = Pfreq return Pfreq_all @@ -263,8 +249,6 @@ def _phase_associator( """ Function to perform phase association for numba implementation. """ - # Setup events datastores: - list_of_curr_event_dfs = [] # Find back-azimuths associated with phase picks: bazis_Z = t_series_df_Z["back_azi"].values[peaks_Z] bazis_hor = t_series_df_hor["back_azi"].values[peaks_hor] @@ -341,7 +325,7 @@ def _phase_associator( filt_events_lst = [] # And loop over events, selecting only max. power events: tmp_count = 0 - for index, row in events_df.iterrows(): + for _, row in events_df.iterrows(): tmp_count += 1 if tmp_count == 1: tmp_lst = [] @@ -1613,6 +1597,7 @@ def detect_events(self, verbosity=0, fnames=None): gc.collect() # Calculate pick thresholds: + mad_pick_threshold_Z = np.median(t_series_df_Z["power"].values) + ( self.mad_multiplier * self._calculate_mad(t_series_df_Z["power"]) ) @@ -2048,4 +2033,4 @@ def get_composite_array_st_from_bazi_slowness( gc.collect() -# ----------------------------------------------- End: Define main functions ----------------------------------------------- +# ------------- End: Define main functions -------------------- From ab854d85ef21137f4687de7a59cafa5837b48207 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 17 Sep 2025 14:25:24 +0100 Subject: [PATCH 089/103] re-implemented MAD as a moving window --- SeisSeeker/processing/detection.py | 54 +++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 7d5aa21..5c3f315 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1598,11 +1598,16 @@ def detect_events(self, verbosity=0, fnames=None): # Calculate pick thresholds: - mad_pick_threshold_Z = np.median(t_series_df_Z["power"].values) + ( - self.mad_multiplier * self._calculate_mad(t_series_df_Z["power"]) + mad_pick_threshold_Z = moving_window_mad( + t_series_df_Z["power"].values, + self.mad_window_length_s, + self.mad_multiplier, ) - mad_pick_threshold_hor = np.median(t_series_df_hor["power"].values) + ( - self.mad_multiplier * self._calculate_mad(t_series_df_hor["power"]) + + mad_pick_threshold_hor = moving_window_mad( + t_series_df_hor["power"].values, + self.mad_window_length_s, + self.mad_multiplier, ) # Get phase picks: @@ -2033,4 +2038,45 @@ def get_composite_array_st_from_bazi_slowness( gc.collect() +def moving_window_mad(trace, window_len, mad_multiplier): + """ + Function to calculate the median absolute deviation (MAD) using a + moving window over a beam power time-series. + + For the start and end of the time-series, where a full window is not + available, the window is truncated to fit within the time-series. + + Note that the MAD is scaled to be equivalent to the standard deviation + for a Gaussian distribution, by multiplying by the constant 1.4826. + See: https://en.wikipedia.org/wiki/Median_absolute_deviation + + Parameters + ---------- + trace : np.ndarray + 1D numpy array containing beam power time-series. + + window_len : int + Length of moving window, in number of samples. + + mad_multiplier : float + Multiplier for MAD to set detection threshold. For example, a value of + 2 would set the threshold to be 2 times the MAD above the median. This is scaled + by 1.4826 within the function to be equivalent to standard deviations for a + Gaussian distribution. + """ + # Create datastore: + mad_thresholds = np.zeros(trace.shape) + half_win = int(window_len / 2) + for i in range(len(trace)): + start = max(0, i - half_win) + end = min(len(trace), i + half_win) + window = trace[start:end] + mad = np.median(np.abs(window - np.median(window))) + # add one to mad_multiplier as the multiplier represents the + # number of standard deviations above the median. + mad_thresholds[i] = MAD_SCALE * (1 + mad_multiplier) * mad + + return mad_thresholds + + # ------------- End: Define main functions -------------------- From 9e77269caca1b11a229fba0c4c877a0c1af8f611 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 17 Sep 2025 14:54:09 +0100 Subject: [PATCH 090/103] added prominence criteria to find_peaks when using MAD --- SeisSeeker/processing/detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 5c3f315..abc3a6d 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1622,11 +1622,13 @@ def detect_events(self, verbosity=0, fnames=None): t_series_df_Z["power"].values, height=mad_pick_threshold_Z, distance=min_pick_dist, + prominence=mad_pick_threshold_Z, ) peaks_hor, _ = find_peaks( t_series_df_hor["power"].values, height=mad_pick_threshold_hor, distance=min_pick_dist, + prominence=mad_pick_threshold_hor, ) # Phase assoicate by BAZI threshold and max. power: From bb3016b6a1c76c4f2d299c76fc41b7096151f5c5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 18 Sep 2025 12:25:22 +0100 Subject: [PATCH 091/103] revert tlib for numba compatiblity reomvoe for loops in _phase_associatior for gettign t in seconds after start --- SeisSeeker/processing/detection.py | 42 ++++++++++++++---------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index abc3a6d..8d62877 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -119,27 +119,25 @@ def _fast_freq_domain_array_proc( utheta_rad = np.deg2rad(utheta) # # Compute time-shifts once: - # tlib = np.zeros((n_stations, n_sl, n_baz), dtype=np.complex128) - # # r, theta as this is polar coord system: - # for ir in range(n_sl): - # for itheta in range(n_baz): - # tlib[:, ir, itheta] = xx * ur[ir] * np.sin(utheta_rad[itheta]) + yy * ur[ - # ir - # ] * np.cos(utheta_rad[itheta]) + tlib = np.zeros((n_stations, n_sl, n_baz), dtype=np.complex128) + # r, theta as this is polar coord system: + for ir in range(n_sl): + for itheta in range(n_baz): + tlib[:, ir, itheta] = xx * ur[ir] * np.sin(utheta_rad[itheta]) + yy * ur[ + ir + ] * np.cos(utheta_rad[itheta]) # Vectorized computation of time-shifts for all stations, slowness, and back-azimuth # xx and yy are (n_stations,) arrays, ur is (n_sl,), utheta_rad is (n_baz,) # We want tlib shape (n_stations, n_sl, n_baz) - xx_ = xx[:, np.newaxis, np.newaxis] # shape (n_stations, 1, 1) - yy_ = yy[:, np.newaxis, np.newaxis] # shape (n_stations, 1, 1) - ur_ = ur[np.newaxis, :, np.newaxis] # shape (1, n_sl, 1) - utheta_rad_ = utheta_rad[np.newaxis, np.newaxis, :] # shape (1, 1, n_baz) - tlib_tmp = xx_ * ur_ * np.sin(utheta_rad_) + yy_ * ur_ * np.cos(utheta_rad_) - # recast array as complex128 - tlib = tlib_tmp.astype(np.complex128) - del tlib_tmp, xx_, yy_, ur_, utheta_rad_ - gc.collect() + # xx_ = xx[:, np.newaxis, np.newaxis] # shape (n_stations, 1, 1) + # yy_ = yy[:, np.newaxis, np.newaxis] # shape (n_stations, 1, 1) + # ur_ = ur[np.newaxis, :, np.newaxis] # shape (1, n_sl, 1) + # utheta_rad_ = utheta_rad[np.newaxis, np.newaxis, :] # shape (1, 1, n_baz) + # tlib_tmp = xx_ * ur_ * np.sin(utheta_rad_) + yy_ * ur_ * np.cos(utheta_rad_) + # recast array as complex128 + # tlib = tlib_tmp.astype(np.complex128) Pfreq_all = np.zeros( (data.shape[0], len(target_freqs), n_sl, n_baz), dtype=np.complex128 ) @@ -258,12 +256,12 @@ def _phase_associator( # Prep. data for numba format: if verbosity > 1: logger.info("Pre-processing time-series") - t_Z_secs_after_start = [] - for index, row in t_series_df_Z.iterrows(): - t_Z_secs_after_start.append( - obspy.UTCDateTime(row["t"]) - obspy.UTCDateTime(t_series_df_Z["t"][0]) - ) - t_hor_secs_after_start = [] + t_Z_secs_after_start = obspy.UTCDateTime(t_series_df_Z["t"]) - obspy.UTCDateTime( + t_series_df_Z["t"][0] + ) + t_hor_secs_after_start = obspy.UTCDateTime( + t_series_df_hor["t"] + ) - obspy.UTCDateTime(t_series_df_hor["t"][0]) for index, row in t_series_df_hor.iterrows(): t_hor_secs_after_start.append( obspy.UTCDateTime(row["t"]) - obspy.UTCDateTime(t_series_df_hor["t"][0]) From ca61d967e2e75cd78a1faa5ff5f7aa18ac46091c Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 18 Sep 2025 12:26:51 +0100 Subject: [PATCH 092/103] linitng and removed useless comment --- SeisSeeker/processing/detection.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 8d62877..e760240 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -262,11 +262,7 @@ def _phase_associator( t_hor_secs_after_start = obspy.UTCDateTime( t_series_df_hor["t"] ) - obspy.UTCDateTime(t_series_df_hor["t"][0]) - for index, row in t_series_df_hor.iterrows(): - t_hor_secs_after_start.append( - obspy.UTCDateTime(row["t"]) - obspy.UTCDateTime(t_series_df_hor["t"][0]) - ) - # Run function: + if verbosity > 1: logger.info("Performing phase association") Z_hor_phase_pair_idxs = _phase_associator_core_worker( From 6ad0a3751e00f8fdc62c2871ae60282ecf59b0c2 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 18 Sep 2025 12:33:00 +0100 Subject: [PATCH 093/103] added convertor to reading dataframes to make time axis utcdatetime not strings --- SeisSeeker/processing/detection.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index e760240..4b630f8 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1500,28 +1500,38 @@ def detect_events(self, verbosity=0, fnames=None): if fname in self.out_fnames_array_proc: # And load in data: # Read in vertical data: - t_series_df_Z = pd.read_csv(fname) + t_series_df_Z = pd.read_csv( + fname, converters={"t": lambda x: obspy.UTCDateTime(x)} + ) # And read in horizontals: try: fname_N = os.path.join( self.outdir, "".join(("detection_t_series_", f_uid, "_chN.csv")) ) - t_series_df_N = pd.read_csv(fname_N) + t_series_df_N = pd.read_csv( + fname_N, converters={"t": lambda x: obspy.UTCDateTime(x)} + ) except FileNotFoundError: fname_N = os.path.join( self.outdir, "".join(("detection_t_series_", f_uid, "_ch1.csv")) ) - t_series_df_N = pd.read_csv(fname_N) + t_series_df_N = pd.read_csv( + fname_N, converters={"t": lambda x: obspy.UTCDateTime(x)} + ) try: fname_E = os.path.join( self.outdir, "".join(("detection_t_series_", f_uid, "_chE.csv")) ) - t_series_df_E = pd.read_csv(fname_E) + t_series_df_E = pd.read_csv( + fname_E, converters={"t": lambda x: obspy.UTCDateTime(x)} + ) except FileNotFoundError: fname_E = os.path.join( self.outdir, "".join(("detection_t_series_", f_uid, "_ch2.csv")) ) - t_series_df_E = pd.read_csv(fname_E) + t_series_df_E = pd.read_csv( + fname_E, converters={"t": lambda x: obspy.UTCDateTime(x)} + ) else: logger.warning(f"fname {fname} not in fname_array_proc list") logger.warning( From 3e683089542377b43afb20ae47e343a625db3503 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 18 Sep 2025 12:35:32 +0100 Subject: [PATCH 094/103] updated caulcation of t in secs after start for _phase_associator --- SeisSeeker/processing/detection.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 4b630f8..8c0b4bf 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -256,12 +256,8 @@ def _phase_associator( # Prep. data for numba format: if verbosity > 1: logger.info("Pre-processing time-series") - t_Z_secs_after_start = obspy.UTCDateTime(t_series_df_Z["t"]) - obspy.UTCDateTime( - t_series_df_Z["t"][0] - ) - t_hor_secs_after_start = obspy.UTCDateTime( - t_series_df_hor["t"] - ) - obspy.UTCDateTime(t_series_df_hor["t"][0]) + t_Z_secs_after_start = np.array(t_series_df_Z["t"] - t_series_df_Z["t"][0]) + t_hor_secs_after_start = np.array(t_series_df_hor["t"] - t_series_df_hor["t"][0]) if verbosity > 1: logger.info("Performing phase association") From f71c2374289ead9f53f3a9074c1b573424851ef5 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 18 Sep 2025 15:57:35 +0100 Subject: [PATCH 095/103] rejigged phase associator to match P pick with best available S pick by bazi differnece and phase seperation (close P and S preferred) --- SeisSeeker/processing/detection.py | 80 +++++++++++++++++------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 8c0b4bf..eb0f7ae 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -212,23 +212,25 @@ def _phase_associator_core_worker( t_hor_secs_after_start[curr_peak_hor_idx] - t_Z_secs_after_start[curr_peak_Z_idx] ) - if curr_t_phase_diff > 0: - if curr_t_phase_diff <= max_phase_sep_s: - # ii. Check if bazis for Z and horizontals current pick match: - if np.abs(bazis_Z[i] - bazis_hor[j]) < bazi_tol: - match = True - # And deal with if they are close to North: - elif np.abs(bazis_Z[i] - bazis_hor[j]) > (360.0 - bazi_tol): - match = True - else: - match = False - + if (curr_t_phase_diff > 0) or (curr_t_phase_diff <= max_phase_sep_s): + # calc bazi diff between Z and H + best_score = np.inf + bazi_diff = min( + np.abs(bazis_Z[i] - bazis_hor[j]), + 360.0 - np.abs(bazis_Z[i] - bazis_hor[j]), + ) + # ii. Check if bazis for Z and horizontals current pick match: + if bazi_diff < bazi_tol: + match_score = ( + bazi_diff / 360 + curr_t_phase_diff / max_phase_sep_s + ) # normalise to 0-1 range + if match_score < best_score: + best_score = match_score + horz_match = curr_peak_hor_idx # And associate phases and create event data if a match is found: - if match: - # Append pair idxs to data store: - Z_hor_phase_pair_idxs.append( - [curr_peak_Z_idx, curr_peak_hor_idx] - ) + if horz_match is not None: + # Append pair idxs to data store: + Z_hor_phase_pair_idxs.append([curr_peak_Z_idx, horz_match]) return Z_hor_phase_pair_idxs @@ -1597,7 +1599,6 @@ def detect_events(self, verbosity=0, fnames=None): gc.collect() # Calculate pick thresholds: - mad_pick_threshold_Z = moving_window_mad( t_series_df_Z["power"].values, self.mad_window_length_s, @@ -1630,8 +1631,7 @@ def detect_events(self, verbosity=0, fnames=None): distance=min_pick_dist, prominence=mad_pick_threshold_hor, ) - - # Phase assoicate by BAZI threshold and max. power: + # Phase associate by BAZI threshold and max. power: events_df = _phase_associator( t_series_df_Z, t_series_df_hor, @@ -1649,58 +1649,65 @@ def detect_events(self, verbosity=0, fnames=None): events_df = self._calc_uncertainties( events_df, t_series_df_Z, t_series_df_hor, verbosity=verbosity ) - - # Append to datastore: - events_df_all = pd.concat([events_df_all, events_df]) - # Plot detected, phase-associated picks: if verbosity > 1: # print("="*40) logger.info("Event phase associations:") # print(events_df) # print("="*40) + time_after_startZ = np.array(t_series_df_Z["t"]) - t_series_df_Z["t"][0] + time_after_startH = ( + np.array(t_series_df_hor["t"]) - t_series_df_hor["t"][0] + ) + fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(9, 6)) # Plot power: ax[0].plot( - t_series_df_Z["t"], t_series_df_Z["power"], label="Vertical power" + time_after_startZ, t_series_df_Z["power"], label="Vertical power" ) ax[0].plot( - t_series_df_hor["t"], + time_after_startH, t_series_df_hor["power"], label="Horizontal power", ) # Plot slowness: ax[1].plot( - t_series_df_Z["t"], + time_after_startZ, t_series_df_Z["slowness"], label="Vertical slowness", ) ax[1].plot( - t_series_df_hor["t"], + time_after_startH, t_series_df_hor["slowness"], label="Horizontal slowness", ) # Plot back-azimuth: ax[2].plot( - t_series_df_Z["t"], + time_after_startZ, t_series_df_Z["back_azi"], label="Vertical back-azimuth", ) ax[2].plot( - t_series_df_hor["t"], + time_after_startH, t_series_df_hor["back_azi"], label="Horizontal back-azimuth", ) - if len(events_df_all) > 0: + if len(events_df) > 0: + events_t1_after_start = ( + np.array(events_df["t1"]) - t_series_df_Z["t"][0] + ) + events_t2_after_start = ( + np.array(events_df["t2"]) - t_series_df_Z["t"][0] + ) ax[0].scatter( - events_df_all["t1"], - np.ones(len(events_df_all)) * np.max(t_series_df_Z["power"]), + events_t1_after_start, + np.ones(len(events_df)) * np.max(t_series_df_Z["power"]), c="r", label="P phase picks", ) ax[0].scatter( - events_df_all["t2"], - np.ones(len(events_df_all)) * np.max(t_series_df_Z["power"]), + events_t2_after_start, + np.ones(len(events_df)) * np.max(t_series_df_Z["power"]), c="b", label="S phase picks", ) @@ -1711,6 +1718,9 @@ def detect_events(self, verbosity=0, fnames=None): ax[0].set_ylabel("Power (arb. units)") ax[1].set_ylabel("Slowness ($km$ $s^{-1}$)") ax[2].set_ylabel("Back-azimuth ($^o$)") + fig.suptitle( + f"Beamforming time-series for {t_series_df_Z['t'][0]} to {t_series_df_Z['t'].values[-1]} UTC" + ) # plt.gca().yaxis.set_major_locator(MaxNLocator(5)) for i in range(3): ax[i].xaxis.set_major_locator(plt.MaxNLocator(3)) @@ -1719,6 +1729,8 @@ def detect_events(self, verbosity=0, fnames=None): fig.savefig(f"{figpath}/Phase_association_{f_uid}.png", dpi=600) plt.show() + # Append to datastore: + events_df_all = pd.concat([events_df_all, events_df]) return events_df_all def create_location_LUTs( From b855830b20d1d4c19bb8d1893d19e5009193538e Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Thu, 18 Sep 2025 16:44:55 +0100 Subject: [PATCH 096/103] revert scoring of matches as its not needed --- SeisSeeker/processing/detection.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index eb0f7ae..ea70be5 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -212,25 +212,18 @@ def _phase_associator_core_worker( t_hor_secs_after_start[curr_peak_hor_idx] - t_Z_secs_after_start[curr_peak_Z_idx] ) - if (curr_t_phase_diff > 0) or (curr_t_phase_diff <= max_phase_sep_s): + if (curr_t_phase_diff > 0) and (curr_t_phase_diff <= max_phase_sep_s): # calc bazi diff between Z and H - best_score = np.inf bazi_diff = min( np.abs(bazis_Z[i] - bazis_hor[j]), 360.0 - np.abs(bazis_Z[i] - bazis_hor[j]), ) # ii. Check if bazis for Z and horizontals current pick match: if bazi_diff < bazi_tol: - match_score = ( - bazi_diff / 360 + curr_t_phase_diff / max_phase_sep_s - ) # normalise to 0-1 range - if match_score < best_score: - best_score = match_score - horz_match = curr_peak_hor_idx + match = [curr_peak_Z_idx, curr_peak_hor_idx] # And associate phases and create event data if a match is found: - if horz_match is not None: - # Append pair idxs to data store: - Z_hor_phase_pair_idxs.append([curr_peak_Z_idx, horz_match]) + # Append pair idxs to data store: + Z_hor_phase_pair_idxs.append(match) return Z_hor_phase_pair_idxs @@ -254,7 +247,7 @@ def _phase_associator( bazis_hor = t_series_df_hor["back_azi"].values[peaks_hor] # ------------------------------------------------------------------- - # Perform core phae association: + # Perform core phase association: # Prep. data for numba format: if verbosity > 1: logger.info("Pre-processing time-series") @@ -1731,6 +1724,7 @@ def detect_events(self, verbosity=0, fnames=None): # Append to datastore: events_df_all = pd.concat([events_df_all, events_df]) + events_df_all.reset_index(drop=True, inplace=True) return events_df_all def create_location_LUTs( From 67a773cf8c912edb0b9b98016b8bff72c92e3d31 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 19 Sep 2025 10:46:07 +0100 Subject: [PATCH 097/103] now sort the fnames so figures are plotted IN TIME ORDER --- SeisSeeker/processing/detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index ea70be5..ac53e18 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1485,7 +1485,7 @@ def detect_events(self, verbosity=0, fnames=None): fnames = glob.glob( os.path.join(self.outdir, "detection_t_series_*_chZ.csv") ) - for fname in fnames: + for fname in sorted(fnames): f_uid = fname[-21:-8] # Check if in list to process: if fname in self.out_fnames_array_proc: From 8c8cfc944bb58b0242f21114f82edc0b9b28145c Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Tue, 30 Sep 2025 10:56:21 +0100 Subject: [PATCH 098/103] fixed error in phase-weighted stacking --- SeisSeeker/processing/detection.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index ac53e18..bd58af6 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -498,32 +498,42 @@ def _create_stacked_data_st(st, Z_all, N_all, E_all): def _create_phase_weighted_stack_st(st, Z_all, N_all, E_all, degree=1): """Function to create stacked data st.""" - Z_inst_phase_all = hilbert(Z_all, axis=1) - N_inst_phase_all = hilbert(N_all, axis=1) - E_inst_phase_all = hilbert(E_all, axis=1) - - Z_phase_stack = np.absolute(np.mean(np.exp(Z_inst_phase_all * 1j), axis=1)) - N_phase_stack = np.absolute(np.mean(np.exp(N_inst_phase_all * 1j), axis=1)) - E_phase_stack = np.absolute(np.mean(np.exp(E_inst_phase_all * 1j), axis=1)) + Z_analytical_signal = hilbert(Z_all, axis=1) + N_analytical_signal = hilbert(N_all, axis=1) + E_analytical_signal = hilbert(E_all, axis=1) + # . Scipy hilbert transform returns the analytical signal with takes the form: + # . s(t) = x(t) + i * y(t) = A(t) * exp(i * phi(t)) + # . where x(t) is the original signal, y(t) is the Hilbert transform of the signal, + # . A(t) is the instantaneous amplitude (envelope) and phi(t) is the instantaneous phase. + + # . We want to calculate the phase stack, which is given by: + # . c(t) = |(1/N) * sum(exp(i * phi_k(t) | ^v + Z_inst_phase = Z_analytical_signal / np.abs(Z_analytical_signal) + N_inst_phase = N_analytical_signal / np.abs(N_analytical_signal) + E_inst_phase = E_analytical_signal / np.abs(E_analytical_signal) + + Z_coherence = np.absolute(np.mean(Z_inst_phase, axis=1)) + N_coherence = np.absolute(np.mean(N_inst_phase, axis=1)) + E_coherence = np.absolute(np.mean(E_inst_phase, axis=1)) composite_st = obspy.Stream() # For Z stacked: tr = st[0].copy() tr.stats.station = "PW-STACK" tr.stats.channel = st[0].stats.channel[0:2] + "Z" - tr.data = np.mean(Z_all, axis=1) * (Z_phase_stack**degree) + tr.data = np.mean(Z_all, axis=1) * (Z_coherence**degree) print(tr.data.shape) composite_st.append(tr) # For N stacked: tr = st[0].copy() tr.stats.station = "PW-STACK" tr.stats.channel = st[0].stats.channel[0:2] + "N" - tr.data = np.mean(N_all, axis=1) * (N_phase_stack**degree) + tr.data = np.mean(N_all, axis=1) * (N_coherence**degree) composite_st.append(tr) # For E stacked: tr = st[0].copy() tr.stats.station = "PW-STACK" tr.stats.channel = st[0].stats.channel[0:2] + "E" - tr.data = np.mean(E_all, axis=1) * (E_phase_stack**degree) + tr.data = np.mean(E_all, axis=1) * (E_coherence**degree) composite_st.append(tr) del tr gc.collect() From 2a4b2579095fc895aa4270db8f5a7f6f91c40b6a Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 22 Oct 2025 13:13:30 +0100 Subject: [PATCH 099/103] fix to min/max baz filter --- SeisSeeker/processing/detection.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index bd58af6..2a337cc 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1093,9 +1093,11 @@ def _find_time_series(self, Psum_all): Returns time-series of coherency (power), slowness and back-azimuth. """ # Calcualte ux, uy: - ur = np.linspace(0, self.max_sl, Psum_all.shape[1]) - utheta = utheta = np.linspace( - 0, 360 - (360 / Psum_all.shape[2]), Psum_all.shape[2] + ur = np.linspace(self.min_sl, self.max_sl, Psum_all.shape[1]) + utheta = np.linspace( + self.min_baz, + self.max_baz - (self.max_baz / Psum_all.shape[2]), + Psum_all.shape[2], ) # Create time-series: n_win_curr = Psum_all.shape[0] @@ -1634,6 +1636,9 @@ def detect_events(self, verbosity=0, fnames=None): distance=min_pick_dist, prominence=mad_pick_threshold_hor, ) + print( + f"Found {len(peaks_Z)} P-phase picks and {len(peaks_hor)} S-phase picks for file with uid {f_uid}" + ) # Phase associate by BAZI threshold and max. power: events_df = _phase_associator( t_series_df_Z, From 764da215b2ffec26428379a313376c8f5f6628b0 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 22 Oct 2025 13:22:47 +0100 Subject: [PATCH 100/103] added outputting of vertical/horizontal powers/slow/backazi for both P and S picks --- SeisSeeker/processing/detection.py | 46 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 2a337cc..2eddc4b 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -273,12 +273,18 @@ def _phase_associator( curr_events = { "t1": [], "t2": [], - "pow1": [], - "pow2": [], - "slow1": [], - "slow2": [], - "bazi1": [], - "bazi2": [], + "t1_powZ": [], + "t1_powH": [], + "t2_powZ": [], + "t2_powH": [], + "t1_slowZ": [], + "t1_slowH": [], + "t2_slowZ": [], + "t2_slowH": [], + "t1_baziZ": [], + "t1_baziH": [], + "t2_baziZ": [], + "t2_baziH": [], } for event_idx in range(len(Z_hor_phase_pair_idxs)): @@ -286,12 +292,18 @@ def _phase_associator( curr_peak_hor_idx = Z_hor_phase_pair_idxs[event_idx][1] curr_events["t1"].append(t_series_df_Z["t"][curr_peak_Z_idx]) curr_events["t2"].append(t_series_df_hor["t"][curr_peak_hor_idx]) - curr_events["pow1"].append(t_series_df_Z["power"][curr_peak_Z_idx]) - curr_events["pow2"].append(t_series_df_hor["power"][curr_peak_hor_idx]) - curr_events["slow1"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) - curr_events["slow2"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) - curr_events["bazi1"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) - curr_events["bazi2"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) + curr_events["t1_powZ"].append(t_series_df_Z["power"][curr_peak_Z_idx]) + curr_events["t1_powH"].append(t_series_df_hor["power"][curr_peak_hor_idx]) + curr_events["t2_powZ"].append(t_series_df_Z["power"][curr_peak_Z_idx]) + curr_events["t2_powH"].append(t_series_df_hor["power"][curr_peak_hor_idx]) + curr_events["t1_slowZ"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) + curr_events["t1_slowH"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) + curr_events["t2_slowZ"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) + curr_events["t2_slowH"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) + curr_events["t1_baziZ"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) + curr_events["t1_baziH"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) + curr_events["t2_baziZ"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) + curr_events["t2_baziH"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) events_df = pd.DataFrame(curr_events) # And tidy: @@ -343,7 +355,9 @@ def _phase_associator( # And remove duplicate S pick associations: # (using same max. power method) # Append summed powers, for sorting: - sum_pows = filt_events_df["pow1"].values + filt_events_df["pow2"].values + sum_pows = ( + filt_events_df["t1_powZ"].values + filt_events_df["t2_powH"].values + ) filt_events_df["sum_pows"] = sum_pows # Remove t2 duplicates, keep highest summed power: filt_events_df.sort_values("sum_pows", inplace=True) @@ -369,9 +383,9 @@ def _find_max_power_event(events): list (of Dataframe Rows) of event """ - pow1_tmp = np.array([event.pow1 for event in events]) - pow2_tmp = np.array([event.pow2 for event in events]) - combined_pows_tmp = pow1_tmp + pow2_tmp + powZ_tmp = np.array([event.t1_powZ for event in events]) + powH_tmp = np.array([event.t2_powH for event in events]) + combined_pows_tmp = powZ_tmp + powH_tmp max_power_idx = np.argmax(combined_pows_tmp) max_power_event = events[max_power_idx] return max_power_event From 5bc524cef4a98227fda59c24b7c561fa80de521a Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Wed, 22 Oct 2025 13:31:19 +0100 Subject: [PATCH 101/103] Revert "fix to min/max baz filter" Revert as adding extra power/bazi columns is not super useful and adds confusion to reading outputs This reverts commit 2a4b2579095fc895aa4270db8f5a7f6f91c40b6a. --- SeisSeeker/processing/detection.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 2eddc4b..079757a 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1107,11 +1107,9 @@ def _find_time_series(self, Psum_all): Returns time-series of coherency (power), slowness and back-azimuth. """ # Calcualte ux, uy: - ur = np.linspace(self.min_sl, self.max_sl, Psum_all.shape[1]) - utheta = np.linspace( - self.min_baz, - self.max_baz - (self.max_baz / Psum_all.shape[2]), - Psum_all.shape[2], + ur = np.linspace(0, self.max_sl, Psum_all.shape[1]) + utheta = utheta = np.linspace( + 0, 360 - (360 / Psum_all.shape[2]), Psum_all.shape[2] ) # Create time-series: n_win_curr = Psum_all.shape[0] @@ -1650,9 +1648,6 @@ def detect_events(self, verbosity=0, fnames=None): distance=min_pick_dist, prominence=mad_pick_threshold_hor, ) - print( - f"Found {len(peaks_Z)} P-phase picks and {len(peaks_hor)} S-phase picks for file with uid {f_uid}" - ) # Phase associate by BAZI threshold and max. power: events_df = _phase_associator( t_series_df_Z, From 5c1adb2184a465657d3584b56f9c0fedee24b285 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 24 Oct 2025 11:35:01 +0100 Subject: [PATCH 102/103] added option to set miniumum phase separetion when associating. default is 0 --- SeisSeeker/processing/detection.py | 67 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 079757a..3ae1ad2 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -195,6 +195,7 @@ def _phase_associator_core_worker( t_Z_secs_after_start, t_hor_secs_after_start, max_phase_sep_s, + min_phase_sep_s, ): """Function to do the heavy lifting of the phase association.""" # Specify data stores: @@ -212,7 +213,11 @@ def _phase_associator_core_worker( t_hor_secs_after_start[curr_peak_hor_idx] - t_Z_secs_after_start[curr_peak_Z_idx] ) - if (curr_t_phase_diff > 0) and (curr_t_phase_diff <= max_phase_sep_s): + if ( + (curr_t_phase_diff > 0) + and (curr_t_phase_diff <= max_phase_sep_s) + and (curr_t_phase_diff >= min_phase_sep_s) + ): # calc bazi diff between Z and H bazi_diff = min( np.abs(bazis_Z[i] - bazis_hor[j]), @@ -236,6 +241,7 @@ def _phase_associator( bazi_tol, filt_phase_assoc_by_max_power, max_phase_sep_s, + min_phase_sep_s, min_event_sep_s, verbosity=0, ): @@ -265,6 +271,7 @@ def _phase_associator( t_Z_secs_after_start, t_hor_secs_after_start, max_phase_sep_s, + min_phase_sep_s, ) # Organise outputs into useful form: if verbosity > 1: @@ -273,18 +280,12 @@ def _phase_associator( curr_events = { "t1": [], "t2": [], - "t1_powZ": [], - "t1_powH": [], - "t2_powZ": [], - "t2_powH": [], - "t1_slowZ": [], - "t1_slowH": [], - "t2_slowZ": [], - "t2_slowH": [], - "t1_baziZ": [], - "t1_baziH": [], - "t2_baziZ": [], - "t2_baziH": [], + "pow1": [], + "pow2": [], + "slow1": [], + "slow2": [], + "bazi1": [], + "bazi2": [], } for event_idx in range(len(Z_hor_phase_pair_idxs)): @@ -292,18 +293,12 @@ def _phase_associator( curr_peak_hor_idx = Z_hor_phase_pair_idxs[event_idx][1] curr_events["t1"].append(t_series_df_Z["t"][curr_peak_Z_idx]) curr_events["t2"].append(t_series_df_hor["t"][curr_peak_hor_idx]) - curr_events["t1_powZ"].append(t_series_df_Z["power"][curr_peak_Z_idx]) - curr_events["t1_powH"].append(t_series_df_hor["power"][curr_peak_hor_idx]) - curr_events["t2_powZ"].append(t_series_df_Z["power"][curr_peak_Z_idx]) - curr_events["t2_powH"].append(t_series_df_hor["power"][curr_peak_hor_idx]) - curr_events["t1_slowZ"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) - curr_events["t1_slowH"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) - curr_events["t2_slowZ"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) - curr_events["t2_slowH"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) - curr_events["t1_baziZ"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) - curr_events["t1_baziH"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) - curr_events["t2_baziZ"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) - curr_events["t2_baziH"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) + curr_events["pow1"].append(t_series_df_Z["power"][curr_peak_Z_idx]) + curr_events["pow2"].append(t_series_df_hor["power"][curr_peak_hor_idx]) + curr_events["slow1"].append(t_series_df_Z["slowness"][curr_peak_Z_idx]) + curr_events["slow2"].append(t_series_df_hor["slowness"][curr_peak_hor_idx]) + curr_events["bazi1"].append(t_series_df_Z["back_azi"][curr_peak_Z_idx]) + curr_events["bazi2"].append(t_series_df_hor["back_azi"][curr_peak_hor_idx]) events_df = pd.DataFrame(curr_events) # And tidy: @@ -355,9 +350,7 @@ def _phase_associator( # And remove duplicate S pick associations: # (using same max. power method) # Append summed powers, for sorting: - sum_pows = ( - filt_events_df["t1_powZ"].values + filt_events_df["t2_powH"].values - ) + sum_pows = filt_events_df["pow1"].values + filt_events_df["pow2"].values filt_events_df["sum_pows"] = sum_pows # Remove t2 duplicates, keep highest summed power: filt_events_df.sort_values("sum_pows", inplace=True) @@ -383,9 +376,9 @@ def _find_max_power_event(events): list (of Dataframe Rows) of event """ - powZ_tmp = np.array([event.t1_powZ for event in events]) - powH_tmp = np.array([event.t2_powH for event in events]) - combined_pows_tmp = powZ_tmp + powH_tmp + pow1_tmp = np.array([event.pow1 for event in events]) + pow2_tmp = np.array([event.pow2 for event in events]) + combined_pows_tmp = pow1_tmp + pow2_tmp max_power_idx = np.argmax(combined_pows_tmp) max_power_event = events[max_power_idx] return max_power_event @@ -731,6 +724,7 @@ def __init__( self.min_event_sep_s = 1.0 self.bazi_tol = 20.0 self.max_phase_sep_s = 2.5 + self.min_phase_sep_s = 0 self.filt_phase_assoc_by_max_power = True self.calc_uncertainties = False # For location: @@ -1107,9 +1101,11 @@ def _find_time_series(self, Psum_all): Returns time-series of coherency (power), slowness and back-azimuth. """ # Calcualte ux, uy: - ur = np.linspace(0, self.max_sl, Psum_all.shape[1]) - utheta = utheta = np.linspace( - 0, 360 - (360 / Psum_all.shape[2]), Psum_all.shape[2] + ur = np.linspace(self.min_sl, self.max_sl, Psum_all.shape[1]) + utheta = np.linspace( + self.min_baz, + self.max_baz - (self.max_baz / Psum_all.shape[2]), + Psum_all.shape[2], ) # Create time-series: n_win_curr = Psum_all.shape[0] @@ -1648,6 +1644,9 @@ def detect_events(self, verbosity=0, fnames=None): distance=min_pick_dist, prominence=mad_pick_threshold_hor, ) + print( + f"Found {len(peaks_Z)} P-phase picks and {len(peaks_hor)} S-phase picks for file with uid {f_uid}" + ) # Phase associate by BAZI threshold and max. power: events_df = _phase_associator( t_series_df_Z, From f4f26188eadbfc9560ee8b801b8b0872f6edb3f9 Mon Sep 17 00:00:00 2001 From: Joseph Asplet Date: Fri, 24 Oct 2025 11:39:28 +0100 Subject: [PATCH 103/103] fixed bug where min_phase_sep_s wasnt in all functions --- SeisSeeker/processing/detection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SeisSeeker/processing/detection.py b/SeisSeeker/processing/detection.py index 3ae1ad2..b754657 100755 --- a/SeisSeeker/processing/detection.py +++ b/SeisSeeker/processing/detection.py @@ -1656,6 +1656,7 @@ def detect_events(self, verbosity=0, fnames=None): self.bazi_tol, self.filt_phase_assoc_by_max_power, self.max_phase_sep_s, + self.min_phase_sep_s, self.min_event_sep_s, verbosity=verbosity, )