├── .gitignore ├── LICENSE.txt ├── README.rst ├── docs ├── Introduction.rst ├── Makefile ├── User_Notes.rst ├── conf.py ├── index.rst ├── lts_classes.rst ├── lts_data_classes.rst ├── ltsva.rst └── requirements.txt ├── example.py ├── lts_array ├── __init__.py ├── classes │ ├── __init__.py │ ├── lts_classes.py │ └── lts_data_class.py ├── ltsva.py └── tools │ ├── __init__.py │ └── lts_array_plot.py ├── readthedocs.yml └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS Finder things 2 | **/.DS_Store 3 | **/._.DS_Store 4 | 5 | # Python setup and runtime things 6 | **/__pycache__ 7 | *egg-info 8 | dist/ 9 | **/build 10 | 11 | # Temporary/swap files from text editors 12 | **/*~ 13 | **/*.swp 14 | **/._* 15 | 16 | # Any image file 17 | *.png 18 | *.jpg 19 | 20 | # Documentation files 21 | **/docs/api 22 | **/_build 23 | **/template 24 | **/static -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Jordan W. Bishop 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | lts_array 2 | ========= 3 | 4 | This package contains a least trimmed squares algorithm written in Python3 and modified for geophysical array processing. An extensive collection 5 | of helper functions is also included. These codes are referenced in 6 | 7 | Bishop, J.W., Fee, D., & Szuberla, C. A. L., (2020). *Improved 8 | infrasound array processing with robust estimators*, Geophys. J. Int., 9 | 221(3) p. 2058-2074 doi: https://doi.org/10.1093/gji/ggaa110 10 | 11 | Documentation for this package can be found `here `__. A broader set of geophysical array processing codes are available 12 | `here `__, which 13 | utilizes this package as the default (and preferred) array processing 14 | algorithm. 15 | 16 | Motivation 17 | ----------------- 18 | 19 | Infrasonic and seismic array processing often relies on the plane wave 20 | assumption. With this assumption, inter-element travel times can be 21 | regressed over station (co-)array coordinates to determine an optimal 22 | back-azimuth and velocity for waves crossing the array. Station errors 23 | such as digitizer timing issues, reversed polarity, and flat channels 24 | can manifest as apparent deviations from the plane wave assumption as 25 | travel time outliers. Additionally, physical deviations from the plane 26 | wave assumption also appear as travel time outliers. This project 27 | identifies these outliers from infrasound (and seismic) arrays through 28 | the *least trimmed squares* robust regression technique. Our python 29 | implementation uses the FAST_LTS algorithm of *Rousseeuw and Van 30 | Driessen (2006)*. Please see *Bishop et al. (2020)* for processing 31 | examples at arrays from the International Monitoring System and Alaska 32 | Volcano Observatory. 33 | 34 | Installation 35 | ------------ 36 | 37 | We recommend using conda and creating a new conda environment such as: 38 | 39 | :: 40 | 41 | conda create -n uafinfra -c conda-forge python=3 obspy numba 42 | 43 | Information on conda environments (and more) is available 44 | `here `__. 45 | 46 | The package `numba` is a new dependency to pull request `23 `__. If you have a previous `uafinfra` environment, you may need to install the `numba `__ package with 47 | 48 | :: 49 | 50 | conda install --name uafinfra numba 51 | 52 | After setting up the conda environment, 53 | `install `__ 54 | the package by running the terminal commands: 55 | 56 | :: 57 | 58 | conda activate uafinfra 59 | git clone https://github.com/uafgeotools/lts_array 60 | cd lts_array 61 | pip install -e . 62 | 63 | This set of commands activates the `uafinfra` conda environment. The final command installs the package in “editable” mode, which means that you can update it with a simple ``git pull`` in your local repository. This install command only needs to be run once. 64 | 65 | Dependencies 66 | ------------ 67 | 68 | - `Python3 `__ 69 | - `ObsPy `__ 70 | - `Numba `__ 71 | 72 | and their dependencies. 73 | 74 | Usage 75 | ----------- 76 | 77 | To access the functions in this package, use the following line (for 78 | example): 79 | 80 | :: 81 | 82 | >> python 83 | import lts_array as lts_array 84 | 85 | Example Processing and Uncertainty Quantification 86 | ---------------------------------------------------------------------- 87 | 88 | See the included ``example.py`` file. The code now automatically calculates uncertainty estimates using the slowness ellipse method of Szuberla and Olson (2004). User notes and more information on uncertainty quantification can be found `here <./docs/_build/html/User_Notes.html#>`__. 89 | 90 | References and Credits 91 | ---------------------- 92 | 93 | If you use this code for array processing, we ask that you cite the 94 | following papers: 95 | 96 | 1. Bishop, J.W., Fee, D., & Szuberla, C. A. L., (2020). Improved 97 | infrasound array processing with robust estimators, Geophys. J. Int., 98 | 221(3) p. 2058-2074 doi: https://doi.org/10.1093/gji/ggaa110 99 | 100 | 2. Rousseeuw, P. J. & Van Driessen, K., 2006. Computing LTS regression 101 | for large data sets, Data Mining and Knowledge Discovery, 12(1), 102 | 29-45 doi: https://doi.org/10.1007/s10618-005-0024-4 103 | 104 | 3. Szuberla, C.A.L. & Olson, J.V., 2004. Uncertainties associated with parameter estimation in atmospheric infrasound arrays, J. Acoust. Soc. Am., 115(1), 253–258. doi: https://doi.org/10.1121/1.1635407 105 | 106 | License 107 | ------- 108 | 109 | MIT (c) 110 | 111 | Authors and Contributors 112 | ------------------------ 113 | 114 | | Jordan W Bishop 115 | | David Fee 116 | | Curt Szuberla 117 | | Liam Toney 118 | 119 | Acknowledgements and Distribution Statement 120 | ------------------------------------------- 121 | 122 | This work was made possible through support provided by the Defense 123 | Threat Reduction Agency Nuclear Arms Control Technology program under 124 | contract HDTRA1-17-C-0031. Distribution Statement A: Approved for public 125 | release; distribution is unlimited. 126 | -------------------------------------------------------------------------------- /docs/Introduction.rst: -------------------------------------------------------------------------------- 1 | lts_array 2 | ========= 3 | 4 | Introduction 5 | ------------------- 6 | 7 | This package contains a least trimmed squares algorithm written in Python3 and modified for geophysical array processing. An extensive collection 8 | of helper functions is also included. These codes are referenced in 9 | 10 | Bishop, J.W., Fee, D., & Szuberla, C. A. L., (2020). *Improved 11 | infrasound array processing with robust estimators*, Geophys. J. Int., 12 | 221(3) p. 2058-2074 doi: https://doi.org/10.1093/gji/ggaa110 13 | 14 | A broader set of geophysical array processing codes are available 15 | `here `__, which 16 | utilizes this package as the default (and preferred) array processing 17 | algorithm. 18 | 19 | Motivation 20 | ----------------- 21 | 22 | Infrasonic and seismic array processing often relies on the plane wave 23 | assumption. With this assumption, inter-element travel times can be 24 | regressed over station (co-)array coordinates to determine an optimal 25 | back-azimuth and velocity for waves crossing the array. Station errors 26 | such as digitizer timing issues, reversed polarity, and flat channels 27 | can manifest as apparent deviations from the plane wave assumption as 28 | travel time outliers. Additionally, physical deviations from the plane 29 | wave assumption also appear as travel time outliers. This method 30 | identifies these outliers from infrasound (and seismic) arrays through 31 | the *least trimmed squares* robust regression technique. Our python 32 | implementation uses the FAST_LTS algorithm of *Rousseeuw and Van 33 | Driessen (2006)*. Uncertainty estimates are calculated using the slowness ellipse method of *Szuberla and Olson (2004)*. Please see *Bishop et al. (2020)* for processing examples at arrays from the International Monitoring System and Alaska Volcano Observatory. 34 | 35 | Installation and Usage 36 | ------------------------------------ 37 | 38 | See the README for installation and usage instructions. 39 | 40 | 41 | References and Credits 42 | ---------------------- 43 | 44 | If you use this code for array processing, we ask that you cite the 45 | following papers: 46 | 47 | 1. Bishop, J.W., Fee, D., & Szuberla, C. A. L., (2020). Improved infrasound array processing with robust estimators, Geophys. J. Int., 221(3) p. 2058-2074 doi: https://doi.org/10.1093/gji/ggaa110 48 | 2. Rousseeuw, P. J. & Van Driessen, K., 2006. Computing LTS regression for large data sets, Data Mining and Knowledge Discovery, 12(1), 29-45 doi: https://doi.org/10.1007/s10618-005-0024-43. 49 | 3. Szuberla, C.A.L. & Olson, J.V., 2004. Uncertainties associated with parameter estimation in atmospheric infrasound arrays, J. Acoust. Soc. Am., 115(1), 253–258. doi: https://doi.org/10.1121/1.1635407 50 | 51 | 52 | License 53 | ------- 54 | 55 | MIT (c) 56 | 57 | 58 | Acknowledgements and Distribution Statement 59 | ------------------------------------------- 60 | 61 | This work was made possible through support provided by the Defense 62 | Threat Reduction Agency Nuclear Arms Control Technology program under 63 | contract HDTRA1-17-C-0031. Distribution Statement A: Approved for public 64 | release; distribution is unlimited. 65 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/User_Notes.rst: -------------------------------------------------------------------------------- 1 | User Notes 2 | ================ 3 | Thanks to helpful feedback from users, here we list a few notes for infrasound array processing with least trimmed squares (LTS). 4 | 5 | 6 | A note on :math:`{\alpha}` 7 | ------------------------------------ 8 | To completely remove one element during LTS processing, set :math:`{\alpha}` = 1 - 2/n. 9 | 10 | 3 Element Arrays 11 | -------------------------- 12 | For 3 element arrays, least trimmed squares cannot be used. This is because we are trying to fit a 2D plane with 3 choose 2 = 3 elements. Ordinary least squares (:math:`{\alpha}` = 1.0) should be used in this case. 13 | 14 | 4 Element Arrays 15 | --------------------------- 16 | For 4 element arrays, least trimmed squares can be used, but its effectiveness is limited. This is because there are 4 choose 2 = 6 data points used in the regression, but each element is involved in 3 cross correlations (no autocorrelations). Maximum trimming , :math:`{\alpha}` = 0.5, here actually chooses 4 elements, so the data will still be contaminated. For this reason, we have added an option to remove an element prior to processing. If a four element array is suspected to have an issue with an element, we recommend the user remove the element and process the array as a 3 element array. 17 | 18 | 5+ Element Arrays 19 | ------------------------------ 20 | LTS is the most effective for processing arrays with at least five elements. 21 | 22 | Uncertainty Quantification 23 | --------------------------------------- 24 | The code now automatically calculates uncertainty estimates using the slowness ellipse method of Szuberla and Olson (2004). The default is currently set at 90% confidence, but this value can be changed in the `LsBeam` class in `lts_classes.py`. Note, the values here are 1/2 the extremal values described in Szuberla and Olson (2004) and are meant to approximate confidence intervals, i.e. value +/- uncertainty estimate, not the area of the coverage ellipse. The :math:`{\sigma_\tau}` value is now calculated automatically by both the ordinary least squares and the least trimmed squares routines. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # -- Path setup -------------------------------------------------------------- 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, os.path.abspath('../../lts_array/')) 8 | 9 | # -- Project information ----------------------------------------------------- 10 | project = 'lts_array' 11 | copyright = 'Jordan W. Bishop, David Fee, and Curt Szuberla' 12 | 13 | # -- General configuration --------------------------------------------------- 14 | 15 | language = 'python' 16 | master_doc = 'index' 17 | 18 | # Removed 'sphinxcontrib.apidoc' 19 | extensions = ['sphinx.ext.autodoc', 20 | 'sphinx.ext.autosummary', 21 | 'sphinx.ext.intersphinx', 22 | 'sphinx.ext.mathjax', 23 | 'sphinx.ext.napoleon', 24 | 'sphinx_rtd_theme', 25 | 'sphinx.ext.viewcode'] 26 | 27 | autodoc_mock_imports = ['numba', 28 | 'numpy', 29 | 'scipy', 30 | 'obspy', 31 | 'matplotlib'] 32 | apidoc_module_dir = '../lts_array' 33 | apidoc_output_dir = 'api' 34 | apidoc_separate_modules = True 35 | apidoc_toc_file = False 36 | 37 | html_theme = 'sphinx_rtd_theme' 38 | 39 | # -- Options for docstrings ------------------------------------------------- 40 | # Docstring Parsing with napoleon 41 | napoleon_google_docstring = True 42 | napoleon_numpy_docstring = False 43 | 44 | # -- URL handling ----------- 45 | intersphinx_mapping = { 46 | 'python': ('https://docs.python.org/3/', None), 47 | 'numpy': ('https://numpy.org/doc/stable/', None), 48 | 'numba': ('https://numba.readthedocs.io/en/stable/', None), 49 | 'scipy': ('https://docs.scipy.org/doc/scipy/', None), 50 | 'obspy': ('https://docs.obspy.org/', None), 51 | 'matplotlib': ('https://matplotlib.org/stable/', None) 52 | } 53 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to lts_array's documentation! 2 | ===================================== 3 | This module describes tools for least squares estimation of plane wave parameters. 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: Contents: 8 | 9 | Introduction.rst 10 | User_Notes.rst 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: API Docs: 15 | 16 | ltsva.rst 17 | lts_classes.rst 18 | lts_data_classes.rst 19 | -------------------------------------------------------------------------------- /docs/lts_classes.rst: -------------------------------------------------------------------------------- 1 | lts_classes 2 | ============== 3 | .. currentmodule:: lts_array.classes.lts_classes 4 | 5 | Documentation for the data processing classes. These classes are used internally by ``ltsva``. 6 | 7 | .. autoclass:: OLSEstimator 8 | :members: 9 | :inherited-members: 10 | 11 | .. autoclass:: LTSEstimator 12 | :members: 13 | :inherited-members: 14 | -------------------------------------------------------------------------------- /docs/lts_data_classes.rst: -------------------------------------------------------------------------------- 1 | lts\_data\_classes 2 | ===================== 3 | .. currentmodule:: lts_array.classes.lts_data_class 4 | 5 | Documentation for the class used to construct the data container. This class are called internally by ``ltsva``. 6 | 7 | .. autoclass:: lts_array.classes.lts_data_class.DataBin 8 | :members: 9 | :inherited-members: 10 | -------------------------------------------------------------------------------- /docs/ltsva.rst: -------------------------------------------------------------------------------- 1 | ltsva 2 | ========= 3 | 4 | .. currentmodule:: lts_array.ltsva 5 | 6 | Documentation for the wrapper function `ltsva`, which stands for Least Trimmed Squares Velocity Azimuth - estimation. This function is the main interface for users to run the processing codes in the package. See the function comments for more details. 7 | 8 | .. autofunction:: lts_array.ltsva 9 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-apidoc 2 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # %% module imports 2 | from obspy.core import UTCDateTime 3 | from obspy.clients.fdsn import Client 4 | 5 | import lts_array 6 | 7 | # User Inputs 8 | # Filter limits [Hz] 9 | FREQ_MIN = 0.5 10 | FREQ_MAX = 5.0 11 | 12 | # Window length [sec] 13 | WINDOW_LENGTH = 30 14 | 15 | # Window overlap decimal [0.0, 1.0) 16 | WINDOW_OVERLAP = 0.50 17 | 18 | # LTS alpha parameter - subset size 19 | ALPHA = 0.5 20 | 21 | # Plot array coordinates 22 | PLOT_ARRAY_COORDINATES = False 23 | 24 | ##################################################### 25 | # End User inputs 26 | ##################################################### 27 | # A short signal recorded at the Alaska Volcano Observatory 28 | # Adak (ADKI) Infrasound Array 29 | NET = 'AV' 30 | STA = 'ADKI' 31 | CHAN = '*' 32 | LOC = '*' 33 | START = UTCDateTime('2019-8-13T19:50') 34 | END = START + 10*60 35 | 36 | # Download data from IRIS 37 | print('Reading in data from IRIS') 38 | client = Client("IRIS") 39 | st = client.get_waveforms(NET, STA, LOC, CHAN, 40 | START, END, attach_response=True) 41 | st.merge(fill_value='latest') 42 | st.trim(START, END, pad='true', fill_value=0) 43 | st.sort() 44 | print(st) 45 | 46 | print('Removing sensitivity...') 47 | st.remove_sensitivity() 48 | 49 | # Filter the data 50 | st.filter("bandpass", freqmin=FREQ_MIN, freqmax=FREQ_MAX, corners=2, zerophase=True) 51 | st.taper(max_percentage=0.05) 52 | 53 | #%% Get inventory and lat/lon info 54 | inv = client.get_stations(network=NET, station=STA, channel=CHAN, 55 | location=LOC, starttime=START, 56 | endtime=END, level='channel') 57 | 58 | lat_list = [] 59 | lon_list = [] 60 | staname = [] 61 | for network in inv: 62 | for station in network: 63 | for channel in station: 64 | lat_list.append(channel.latitude) 65 | lon_list.append(channel.longitude) 66 | staname.append(channel.code) 67 | 68 | 69 | # Flip a channel for testing 70 | st[3].data *= -1 71 | 72 | # Run processing 73 | lts_vel, lts_baz, t, mdccm, stdict, sigma_tau, conf_int_vel, conf_int_baz = lts_array.ltsva(st, lat_list, lon_list, WINDOW_LENGTH, WINDOW_OVERLAP, ALPHA, PLOT_ARRAY_COORDINATES) 74 | 75 | # Plot the results 76 | fig, axs = lts_array.tools.lts_array_plot(st, lts_vel, lts_baz, t, mdccm, stdict) 77 | # Plot uncertainty estimates 78 | axs[1].plot(t, lts_vel + conf_int_vel, c='gray', linestyle=':') 79 | axs[1].plot(t, lts_vel - conf_int_vel, c='gray', linestyle=':') 80 | axs[2].plot(t, lts_baz + conf_int_baz, c='gray', linestyle=':') 81 | axs[2].plot(t, lts_baz - conf_int_baz, c='gray', linestyle=':') 82 | 83 | """ 84 | Note that our flipped element is dropped in both data 85 | windows that include the signal. 86 | """ 87 | -------------------------------------------------------------------------------- /lts_array/__init__.py: -------------------------------------------------------------------------------- 1 | from . import classes 2 | from . import tools 3 | from . ltsva import ltsva 4 | -------------------------------------------------------------------------------- /lts_array/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from . lts_classes import * 2 | from . lts_data_class import * 3 | -------------------------------------------------------------------------------- /lts_array/classes/lts_classes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import lstsq 3 | from scipy.special import erfinv 4 | from numba import jit 5 | 6 | # Layout: 7 | # 1) jit-decorated functions 8 | # 2) functions 9 | # 3) class definitions 10 | 11 | ######################## 12 | # jit-decorated functions 13 | ######################## 14 | 15 | 16 | @jit(nopython=True) 17 | def random_set(tot, npar, seed): 18 | """ Generate a random data subset for LTS. """ 19 | randlist = [] 20 | for ii in range(0, npar): 21 | seed = np.floor(seed * 5761) + 999 22 | quot = np.floor(seed / 65536) 23 | seed = np.floor(seed) - np.floor(quot * 65536) 24 | random = float(seed / 65536) 25 | num = np.floor(random * tot) 26 | if ii > 0: 27 | while num in randlist: 28 | seed = np.floor(seed * 5761) + 999 29 | quot = np.floor(seed / 65536) 30 | seed = np.floor(seed) - np.floor(quot * 65536) 31 | random = float(seed / 65536) 32 | num = np.floor(random * tot) 33 | randlist.append(num) 34 | randset = np.array(randlist, dtype=np.int64) 35 | 36 | return randset, np.int64(seed) 37 | 38 | 39 | @jit(nopython=True) 40 | def check_array(candidate_size, best_coeff, best_obj, z, obj): 41 | """ Keep best coefficients for final C-step iteration. 42 | Don't keep duplicates. 43 | """ 44 | insert = True 45 | for kk in range(0, candidate_size): 46 | if (best_obj[kk] == obj) and ((best_coeff[:, kk] == z).all()): 47 | insert = False 48 | if insert: 49 | bobj = np.concatenate((best_obj, np.array([obj]))) 50 | bcoeff = np.concatenate((best_coeff, z), axis=1) 51 | idx = np.argsort(bobj)[0:candidate_size] 52 | bobj = bobj[idx] 53 | bcoeff = bcoeff[:, idx] 54 | 55 | return bcoeff, bobj 56 | 57 | 58 | @jit(nopython=True) 59 | def fast_LTS(nits, tau, time_delay_mad, xij_standardized, xij_mad, dimension_number, candidate_size, n_samples, co_array_num, slowness_coeffs, csteps, h, csteps2, random_set, _insertion): 60 | """ Run the FAST_LTS algorithm to determine an initial optimal slowness vector. 61 | """ 62 | for jj in range(nits): 63 | 64 | # Check for data spike. 65 | if (time_delay_mad[jj] == 0) or (np.count_nonzero(tau[:, jj, :]) < (co_array_num - 2)): 66 | # We have a data spike, so do not process. 67 | continue 68 | 69 | # Standardize the y-values 70 | y_var = tau[:, jj, :] / time_delay_mad[jj] 71 | X_var = xij_standardized 72 | 73 | objective_array = np.full((candidate_size, ), np.inf) 74 | coeff_array = np.full((dimension_number, candidate_size), np.nan) # noqa 75 | # Initial seed for random search 76 | seed = 0 77 | # Initial best objective function value 78 | best_objective = np.inf 79 | # Initial search through the subsets 80 | for ii in range(0, n_samples): 81 | prev_obj = 0 82 | # Initial random solution 83 | index, seed = random_set(co_array_num, dimension_number, seed) # noqa 84 | q, r = np.linalg.qr(X_var[index, :]) 85 | qt = q.conj().T @ y_var[index] 86 | z = np.linalg.lstsq(r, qt)[0] 87 | residuals = y_var - X_var @ z 88 | # Perform C-steps 89 | for kk in range(0, csteps): 90 | sortind = np.argsort(np.abs(residuals).flatten()) # noqa 91 | obs_in_set = sortind.flatten()[0:h] 92 | q, r = np.linalg.qr(X_var[obs_in_set, :]) 93 | qt = q.conj().T @ y_var[obs_in_set] 94 | z = np.linalg.lstsq(r, qt)[0] 95 | residuals = y_var - X_var @ z 96 | # Sort the residuals in magnitude from low to high 97 | sor = np.sort(np.abs(residuals).flatten()) # noqa 98 | # Sum the first "h" squared residuals 99 | obj = np.sum(sor[0:h]**2) 100 | # Stop if the C-steps have converged 101 | if (kk >= 1) and (obj == prev_obj): 102 | break 103 | prev_obj = obj 104 | # Save these initial estimates for future C-steps in next round # noqa 105 | if obj < np.max(objective_array): 106 | # Save the best objective function values. 107 | coeff_array, objective_array = check_array( 108 | candidate_size, coeff_array, objective_array, z, obj) # noqa 109 | 110 | # Final condensation of promising data points 111 | for ii in range(0, candidate_size): 112 | prev_obj = 0 113 | if np.isfinite(objective_array[ii]): 114 | z = coeff_array[:, ii].copy() 115 | z = z.reshape((len(z), 1)) 116 | else: 117 | index, seed = random_set(co_array_num, dimension_number, seed) # noqa 118 | q, r = np.linalg.qr(X_var[index, :]) 119 | qt = q.conj().T @ y_var[index] 120 | z = np.linalg.lstsq(r, qt)[0] 121 | 122 | if np.isfinite(z[0]): 123 | residuals = y_var - X_var @ z 124 | # Perform C-steps 125 | for kk in range(0, csteps2): 126 | sort_ind = np.argsort(np.abs(residuals).flatten()) # noqa 127 | obs_in_set = sort_ind.flatten()[0:h] 128 | q, r = np.linalg.qr(X_var[obs_in_set, :]) 129 | qt = q.conj().T @ y_var[obs_in_set] 130 | z = np.linalg.lstsq(r, qt)[0] 131 | residuals = y_var - X_var @ z 132 | # Sort the residuals in magnitude from low to high 133 | sor = np.sort(np.abs(residuals).flatten()) # noqa 134 | # Sum the first "h" squared residuals 135 | obj = np.sum(sor[0:h]**2) 136 | # Stop if the C-steps have converged 137 | if (kk >= 1) and (obj == prev_obj): 138 | break 139 | prev_obj = obj 140 | if obj < best_objective: 141 | best_objective = obj 142 | coeffs = z.copy() 143 | 144 | # Correct coefficients due to standardization 145 | for ii in range(0, dimension_number): 146 | coeffs[ii] *= time_delay_mad[jj] / xij_mad[ii] 147 | 148 | slowness_coeffs[:, jj] = coeffs.flatten() 149 | 150 | return slowness_coeffs 151 | 152 | 153 | ########### 154 | # functions 155 | ########### 156 | def raw_corfactor_lts(p, n, ALPHA): 157 | r""" Calculates the correction factor (from Pison et al. 2002) 158 | to make the LTS solution unbiased for small n. 159 | 160 | Args: 161 | p (int): The rank of X, the number of parameters to fit. 162 | n (int): The number of data points used in processing. 163 | ALPHA (float): The percentage of data points to keep in 164 | the LTS, e.g. h = floor(ALPHA*n). 165 | 166 | Returns: 167 | (float): 168 | ``finitefactor``: A correction factor to make the LTS 169 | solution approximately unbiased for small (i.e. finite n). 170 | 171 | """ 172 | 173 | # ALPHA = 0.875. 174 | coeffalpha875 = np.array([ 175 | [-0.251778730491252, -0.146660023184295], 176 | [0.883966931611758, 0.86292940340761], [3, 5]]) 177 | # ALPHA = 0.500. 178 | coeffalpha500 = np.array([ 179 | [-0.487338281979106, -0.340762058011], 180 | [0.405511279418594, 0.37972360544988], [3, 5]]) 181 | 182 | # Apply eqns (6) and (7) from Pison et al. (2002) 183 | y1_500 = 1 + coeffalpha500[0, 0] / np.power(p, coeffalpha500[1, 0]) 184 | y2_500 = 1 + coeffalpha500[0, 1] / np.power(p, coeffalpha500[1, 1]) 185 | y1_875 = 1 + coeffalpha875[0, 0] / np.power(p, coeffalpha875[1, 0]) 186 | y2_875 = 1 + coeffalpha875[0, 1] / np.power(p, coeffalpha875[1, 1]) 187 | 188 | # Solve for new ALPHA = 0.5 coefficients for the input p. 189 | y1_500 = np.log(1 - y1_500) 190 | y2_500 = np.log(1 - y2_500) 191 | y_500 = np.array([[y1_500], [y2_500]]) 192 | X_500 = np.array([ # noqa 193 | [1, np.log(1/(coeffalpha500[2, 0]*p**2))], 194 | [1, np.log(1/(coeffalpha500[2, 1]*p**2))]]) 195 | c500 = np.linalg.lstsq(X_500, y_500, rcond=-1)[0] 196 | 197 | # Solve for new ALPHA = 0.875 coefficients for the input p. 198 | y1_875 = np.log(1 - y1_875) 199 | y2_875 = np.log(1 - y2_875) 200 | y_875 = np.array([[y1_875], [y2_875]]) 201 | X_875 = np.array([ # noqa 202 | [1, np.log(1 / (coeffalpha875[2, 0] * p**2))], 203 | [1, np.log(1 / (coeffalpha875[2, 1] * p**2))]]) 204 | c875 = np.linalg.lstsq(X_875, y_875, rcond=-1)[0] 205 | 206 | # Get new correction factors for the specified n. 207 | fp500 = 1 - np.exp(c500[0]) / np.power(n, c500[1]) 208 | fp875 = 1 - np.exp(c875[0]) / np.power(n, c875[1]) 209 | 210 | # Linearly interpolate for the specified ALPHA. 211 | if (ALPHA >= 0.500) and (ALPHA <= 0.875): 212 | fpfinal = fp500 + ((fp875 - fp500) / 0.375)*(ALPHA - 0.500) 213 | 214 | if (ALPHA > 0.875) and (ALPHA < 1): 215 | fpfinal = fp875 + ((1 - fp875) / 0.125)*(ALPHA - 0.875) 216 | 217 | finitefactor = np.ndarray.item(1 / fpfinal) 218 | return finitefactor 219 | 220 | 221 | def raw_consfactor_lts(h, n): 222 | r""" Calculate the constant used to make the 223 | LTS scale estimators consistent for 224 | a normal distribution. 225 | 226 | Args: 227 | h (int): The number of points to fit. 228 | n (int): The total number of data points. 229 | 230 | Returns: 231 | (float): 232 | ``dhn``: The correction factor d_h,n. 233 | 234 | """ 235 | # Calculate the initial factor c_h,n. 236 | x = (h + n) / (2 * n) 237 | phinv = np.sqrt(2) * erfinv(2 * x - 1) 238 | chn = 1 / phinv 239 | 240 | # Calculate d_h,n. 241 | phi = (1 / np.sqrt(2 * np.pi)) * np.exp((-1 / 2) * phinv**2) 242 | d = np.sqrt(1 - (2 * n / (h * chn)) * phi) 243 | dhn = 1 / d 244 | 245 | return dhn 246 | 247 | 248 | def _qnorm(p, s=1, m=0): 249 | r""" The normal inverse distribution function. """ 250 | x = erfinv(2 * p - 1) * np.sqrt(2) * s + m 251 | return x 252 | 253 | 254 | def _dnorm(x, s=1, m=0): 255 | r""" The normal density function. """ 256 | c = (1 / (np.sqrt(2 * np.pi) * s)) * np.exp(-0.5 * ((x - m) / s)**2) 257 | return c 258 | 259 | 260 | def rew_corfactor_lts(p, n, ALPHA): 261 | r""" Correction factor for final LTS least-squares fit. 262 | 263 | Args: 264 | p (int): The rank of X, the number of parameters to fit. 265 | intercept (int): Logical. Are you fitting an intercept? 266 | Set to false for array processing. 267 | n (int): The number of data points used in processing. 268 | ALPHA (float): The percentage of data points to keep in 269 | the LTS, e.g. h = floor(ALPHA*n). 270 | 271 | Returns: 272 | (float): 273 | ``finitefactor``: A finite sample correction factor. 274 | 275 | """ 276 | 277 | # ALPHA = 0.500. 278 | coeffalpha500 = np.array([ 279 | [-0.417574780492848, -0.175753709374146], 280 | [1.83958876341367, 1.8313809497999], [3, 5]]) 281 | 282 | # ALPHA = 0.875. 283 | coeffalpha875 = np.array([ 284 | [-0.267522855927958, -0.161200683014406], 285 | [1.17559984533974, 1.21675019853961], [3, 5]]) 286 | 287 | # Apply eqns (6) and (7) from Pison et al. (2002). 288 | y1_500 = 1 + coeffalpha500[0, 0] / np.power(p, coeffalpha500[1, 0]) 289 | y2_500 = 1 + coeffalpha500[0, 1] / np.power(p, coeffalpha500[1, 1]) 290 | y1_875 = 1 + coeffalpha875[0, 0] / np.power(p, coeffalpha875[1, 0]) 291 | y2_875 = 1 + coeffalpha875[0, 1] / np.power(p, coeffalpha875[1, 1]) 292 | 293 | # Solve for new ALPHA = 0.5 coefficients for the input p. 294 | y1_500 = np.log(1 - y1_500) 295 | y2_500 = np.log(1 - y2_500) 296 | y_500 = np.array([[y1_500], [y2_500]]) 297 | X_500 = np.array([ # noqa 298 | [1, np.log(1 / (coeffalpha500[2, 0] * p**2))], 299 | [1, np.log(1 / (coeffalpha500[2, 1] * p**2))]]) 300 | c500 = np.linalg.lstsq(X_500, y_500, rcond=-1)[0] 301 | 302 | # Solve for new ALPHA = 0.875 coefficients for the input p. 303 | y1_875 = np.log(1 - y1_875) 304 | y2_875 = np.log(1 - y2_875) 305 | y_875 = np.array([[y1_875], [y2_875]]) 306 | X_875 = np.array([ # noqa 307 | [1, np.log(1 / (coeffalpha875[2, 0] * p**2))], 308 | [1, np.log(1 / (coeffalpha875[2, 1] * p**2))]]) 309 | c875 = np.linalg.lstsq(X_875, y_875, rcond=-1)[0] 310 | 311 | # Get new correction functions for the specified n. 312 | fp500 = 1 - np.exp(c500[0]) / np.power(n, c500[1]) 313 | fp875 = 1 - np.exp(c875[0]) / np.power(n, c875[1]) 314 | 315 | # Linearly interpolate for the specified ALPHA. 316 | if (ALPHA >= 0.500) and (ALPHA <= 0.875): 317 | fpfinal = fp500 + ((fp875 - fp500) / 0.375)*(ALPHA - 0.500) 318 | 319 | if (ALPHA > 0.875) and (ALPHA < 1): 320 | fpfinal = fp875 + ((1 - fp875) / 0.125)*(ALPHA - 0.875) 321 | 322 | finitefactor = np.ndarray.item(1 / fpfinal) 323 | return finitefactor 324 | 325 | 326 | def rew_consfactor_lts(weights, p, n): 327 | r""" Another correction factor for the final LTS fit. 328 | 329 | Args: 330 | weights (array): The standardized residuals. 331 | n (int): The total number of data points. 332 | p (int): The number of parameters to estimate. 333 | 334 | Returns: 335 | (float): 336 | ``cdelta_rew``: A small sample correction factor. 337 | 338 | """ 339 | a = _dnorm(1 / (1 / (_qnorm((sum(weights) + n) / (2 * n))))) 340 | b = (1 / _qnorm((np.sum(weights) + n) / (2 * n))) 341 | q = 1 - ((2 * n) / (np.sum(weights) * b)) * a 342 | cdelta_rew = 1/np.sqrt(q) 343 | 344 | return cdelta_rew 345 | 346 | 347 | def cubicEqn(a, b, c): 348 | r""" 349 | Roots of cubic equation in the form :math:`x^3 + ax^2 + bx + c = 0`. 350 | 351 | Args: 352 | a (int or float): Scalar coefficient of cubic equation, can be 353 | complex 354 | b (int or float): Same as above 355 | c (int or float): Same as above 356 | 357 | Returns: 358 | list: Roots of cubic equation in standard form 359 | 360 | See Also: 361 | :func:`numpy.roots` — Generic polynomial root finder 362 | 363 | Notes: 364 | Relatively stable solutions, with some tweaks by Dr. Z, 365 | per algorithm of Numerical Recipes 2nd ed., :math:`\S` 5.6. Even 366 | :func:`numpy.roots` can have some (minor) issues; e.g., 367 | :math:`x^3 - 5x^2 + 8x - 4 = 0`. 368 | """ 369 | 370 | Q = a*a/9 - b/3 371 | R = (3*c - a*b)/6 + a*a*a/27 372 | Q3 = Q*Q*Q 373 | R2 = R*R 374 | ao3 = a/3 375 | 376 | # Q & R are real 377 | if np.isreal([a, b, c]).all(): 378 | # 3 real roots 379 | if R2 < Q3: 380 | sqQ = -2 * np.sqrt(Q) 381 | theta = np.arccos(R / np.sqrt(Q3)) 382 | # This solution first published in 1615 by Viète! 383 | x = [sqQ * np.cos(theta / 3) - ao3, 384 | sqQ * np.cos((theta + 2 * np.pi) / 3) - ao3, 385 | sqQ * np.cos((theta - 2 * np.pi) / 3) - ao3] 386 | # Q & R real, but 1 real, 2 complex roots 387 | else: 388 | # this is req'd since np.sign(0) = 0 389 | if R != 0: 390 | A = -np.sign(R) * (np.abs(R) + np.sqrt(R2 - Q3)) ** (1 / 3) 391 | else: 392 | A = -np.sqrt(-Q3) ** (1 / 3) 393 | if A == 0: 394 | B = 0 395 | else: 396 | B = Q/A 397 | # one real root & two conjugate complex ones 398 | x = [ 399 | (A+B) - ao3, 400 | -.5 * (A+B) + 1j * np.sqrt(3) / 2 * (A - B) - ao3, 401 | -.5 * (A+B) - 1j * np.sqrt(3) / 2 * (A - B) - ao3] 402 | # Q & R complex, so also 1 real, 2 complex roots 403 | else: 404 | sqR2mQ3 = np.sqrt(R2 - Q3) 405 | if np.real(np.conj(R) * sqR2mQ3) >= 0: 406 | A = -(R+sqR2mQ3)**(1/3) 407 | else: 408 | A = -(R-sqR2mQ3)**(1/3) 409 | if A == 0: 410 | B = 0 411 | else: 412 | B = Q/A 413 | # one real root & two conjugate complex ones 414 | x = [ 415 | (A+B) - ao3, 416 | -.5 * (A+B) + 1j * np.sqrt(3) / 2 * (A - B) - ao3, 417 | -.5 * (A+B) - 1j * np.sqrt(3) / 2 * (A - B) - ao3 418 | ] 419 | # parse real and/or int roots for tidy output 420 | for k in range(0, 3): 421 | if np.real(x[k]) == x[k]: 422 | x[k] = float(np.real(x[k])) 423 | if int(x[k]) == x[k]: 424 | x[k] = int(x[k]) 425 | return x 426 | 427 | 428 | def quadraticEqn(a, b, c): 429 | r""" 430 | Roots of quadratic equation in the form :math:`ax^2 + bx + c = 0`. 431 | 432 | Args: 433 | a (int or float): Scalar coefficient of quadratic equation, can be 434 | complex 435 | b (int or float): Same as above 436 | c (int or float): Same as above 437 | 438 | Returns: 439 | list: Roots of quadratic equation in standard form 440 | 441 | See Also: 442 | :func:`numpy.roots` — Generic polynomial root finder 443 | 444 | Notes: 445 | Stable solutions, even for :math:`b^2 >> ac` or complex coefficients, 446 | per algorithm of Numerical Recipes 2nd ed., :math:`\S` 5.6. 447 | """ 448 | 449 | # real coefficient branch 450 | if np.isreal([a, b, c]).all(): 451 | # note np.sqrt(-1) = nan, so force complex argument 452 | if b: 453 | # std. sub-branch 454 | q = -0.5*(b + np.sign(b) * np.sqrt(complex(b * b - 4 * a * c))) 455 | else: 456 | # b = 0 sub-branch 457 | q = -np.sqrt(complex(-a * c)) 458 | # complex coefficient branch 459 | else: 460 | if np.real(np.conj(b) * np.sqrt(b * b - 4 * a * c)) >= 0: 461 | q = -0.5*(b + np.sqrt(b * b - 4 * a * c)) 462 | else: 463 | q = -0.5*(b - np.sqrt(b * b - 4 * a * c)) 464 | # stable root solution 465 | x = [q/a, c/q] 466 | # parse real and/or int roots for tidy output 467 | for k in 0, 1: 468 | if np.real(x[k]) == x[k]: 469 | x[k] = float(np.real(x[k])) 470 | if int(x[k]) == x[k]: 471 | x[k] = int(x[k]) 472 | return x 473 | 474 | 475 | def quarticEqn(a, b, c, d): 476 | r""" 477 | Roots of quartic equation in the form :math:`x^4 + ax^3 + bx^2 + 478 | cx + d = 0`. 479 | 480 | Args: 481 | a (int or float): Scalar coefficient of quartic equation, can be 482 | complex 483 | b (int or float): Same as above 484 | c (int or float): Same as above 485 | d (int or float): Same as above 486 | 487 | Returns: 488 | list: Roots of quartic equation in standard form 489 | 490 | See Also: 491 | :func:`numpy.roots` — Generic polynomial root finder 492 | 493 | Notes: 494 | Stable solutions per algorithm of CRC Std. Mathematical Tables, 29th 495 | ed. 496 | """ 497 | 498 | # find *any* root of resolvent cubic 499 | a2 = a*a 500 | y = cubicEqn(-b, a*c - 4*d, (4*b - a2)*d - c*c) 501 | y = y[0] 502 | # find R 503 | R = np.sqrt(a2 / 4 - (1 + 0j) * b + y) # force complex in sqrt 504 | foo = 3*a2/4 - R*R - 2*b 505 | if R != 0: 506 | # R is already complex. 507 | D = np.sqrt(foo + (a * b - 2 * c - a2 * a / 4) / R) 508 | E = np.sqrt(foo - (a * b - 2 * c - a2 * a / 4) / R) # ... 509 | else: 510 | sqrtTerm = 2 * np.sqrt(y * y - (4 + 0j) * d) # force complex in sqrt 511 | D = np.sqrt(foo + sqrtTerm) 512 | E = np.sqrt(foo - sqrtTerm) 513 | x = [-a/4 + R/2 + D/2, 514 | -a/4 + R/2 - D/2, 515 | -a/4 - R/2 + E/2, 516 | -a/4 - R/2 - E/2] 517 | # parse real and/or int roots for tidy output 518 | for k in range(0, 4): 519 | if np.real(x[k]) == x[k]: 520 | x[k] = float(np.real(x[k])) 521 | if int(x[k]) == x[k]: 522 | x[k] = int(x[k]) 523 | 524 | return x 525 | 526 | 527 | def rthEllipse(a, b, x0, y0): 528 | r""" 529 | Calculate angles subtending, and extremal distances to, a 530 | coordinate-aligned ellipse from the origin. 531 | 532 | Args: 533 | a (float): Semi-major axis of ellipse 534 | b (float): Semi-minor axis of ellipse 535 | x0 (float): Horizontal center of ellipse 536 | y0 (float): Vertical center of ellipse 537 | 538 | Returns: 539 | tuple: Tuple containing: 540 | 541 | - **eExtrm** – Extremal parameters in ``(4, )`` array as 542 | 543 | .. code-block:: none 544 | 545 | [min distance, max distance, min angle (degrees), max angle (degrees)] 546 | 547 | - **eVec** – Coordinates of extremal points on ellipse in ``(4, 2)`` 548 | array as 549 | 550 | .. code-block:: none 551 | 552 | [[x min dist., y min dist.], 553 | [x max dist., y max dist.], 554 | [x max angle tangency, y max angle tangency], 555 | [x min angle tangency, y min angle tangency]] 556 | """ 557 | 558 | # set constants 559 | A = 2/a**2 560 | B = 2*x0/a**2 561 | C = 2/b**2 562 | D = 2*y0/b**2 563 | E = (B*x0+D*y0)/2-1 564 | F = C-A 565 | G = A/2 566 | H = C/2 567 | eExtrm = np.zeros((4,)) 568 | eVec = np.zeros((4, 2)) 569 | eps = np.finfo(np.float64).eps 570 | 571 | # some tolerances for numerical errors 572 | circTol = 1e8 # is it circular to better than circTol*eps? 573 | zeroTol = 1e4 # is center along a coord. axis to better than zeroTol*eps? 574 | magTol = 1e-5 # is a sol'n within ellipse*(1+magTol) (magnification) 575 | 576 | # pursue circular or elliptical solutions 577 | if np.abs(F) <= circTol * eps: 578 | # circle 579 | cent = np.sqrt(x0 ** 2 + y0 ** 2) 580 | eExtrm[0:2] = cent + np.array([-a, a]) 581 | eVec[0:2, :] = np.array([ 582 | [x0-a*x0/cent, y0-a*y0/cent], 583 | [x0+a*x0/cent, y0+a*y0/cent]]) 584 | else: 585 | # ellipse 586 | # check for trivial distance sol'n 587 | if np.abs(y0) < zeroTol * eps: 588 | eExtrm[0:2] = x0 + np.array([-a, a]) 589 | eVec[0:2, :] = np.vstack((eExtrm[0:2], [0, 0])).T 590 | elif np.abs(x0) < zeroTol * eps: 591 | eExtrm[0:2] = y0 + np.array([-b, b]) 592 | eVec[0:2, :] = np.vstack(([0, 0], eExtrm[0:2])).T 593 | else: 594 | # use dual solutions of quartics to find best, real-valued results 595 | # solve quartic for y 596 | fy = F**2*H 597 | y = quarticEqn(-D*F*(2*H+F)/fy, 598 | (B**2*(G+F)+E*F**2+D**2*(H+2*F))/fy, 599 | -D*(B**2+2*E*F+D**2)/fy, (D**2*E)/fy) 600 | y = np.array([y[i] for i in list(np.where(y == np.real(y))[0])]) 601 | xy = B*y / (D-F*y) 602 | # solve quartic for x 603 | fx = F**2*G 604 | x = quarticEqn(B*F*(2*G-F)/fx, (B**2*(G-2*F)+E*F**2+D**2*(H-F))/fx, 605 | B*(2*E*F-B**2-D**2)/fx, (B**2*E)/fx) 606 | x = np.array([x[i] for i in list(np.where(x == np.real(x))[0])]) 607 | yx = D*x / (F*x+B) 608 | # combine both approaches 609 | distE = np.hstack( 610 | (np.sqrt(x ** 2 + yx ** 2), np.sqrt(xy ** 2 + y ** 2))) 611 | # trap real, but bogus sol's (esp. near Th = 180) 612 | distEidx = np.where( 613 | (distE <= np.sqrt(x0 ** 2 + y0 ** 2) 614 | + np.max([a, b]) * (1 + magTol)) 615 | & (distE >= np.sqrt(x0 ** 2 + y0 ** 2) 616 | - np.max([a, b]) * (1 + magTol))) 617 | coords = np.hstack(((x, yx), (xy, y))).T 618 | coords = coords[distEidx, :][0] 619 | distE = distE[distEidx] 620 | eExtrm[0:2] = [distE.min(), distE.max()] 621 | eVec[0:2, :] = np.vstack( 622 | (coords[np.where(distE == distE.min()), :][0][0], 623 | coords[np.where(distE == distE.max()), :][0][0])) 624 | # angles subtended 625 | if x0 < 0: 626 | x0 = -x0 627 | y = -np.array(quadraticEqn(D ** 2 + B ** 2 * H / G, 4 * D * E, 628 | 4 * E ** 2 - B ** 2 * E / G)) 629 | x = -np.sqrt(E / G - H / G * y ** 2) 630 | else: 631 | y = -np.array(quadraticEqn(D ** 2 + B ** 2 * H / G, 4 * D * E, 632 | 4 * E ** 2 - B ** 2 * E / G)) 633 | x = np.sqrt(E / G - H / G * y ** 2) 634 | eVec[2:, :] = np.vstack((np.real(x), np.real(y))).T 635 | # various quadrant fixes 636 | if x0 == 0 or np.abs(x0) - a < 0: 637 | eVec[2, 0] = -eVec[2, 0] 638 | eExtrm[2:] = np.sort(np.arctan2(eVec[2:, 1], eVec[2:, 0]) / np.pi * 180) 639 | 640 | return eExtrm, eVec 641 | 642 | 643 | def post_process(dimension_number, co_array_num, alpha, h, nits, tau, xij, coeffs, lts_vel, lts_baz, element_weights, sigma_tau, p, conf_int_vel, conf_int_baz): 644 | 645 | # Initial fit - correction factor to make LTS approximately unbiased 646 | raw_factor = raw_corfactor_lts(dimension_number, co_array_num, alpha) 647 | # Initial fit - correction factor to make LTS approximately normally distributed # noqa 648 | raw_factor *= raw_consfactor_lts(h, co_array_num) 649 | # Final fit - correction factor to make LTS approximately unbiased 650 | rew_factor1 = rew_corfactor_lts(dimension_number, co_array_num, alpha) 651 | # Value of the normal inverse distribution function 652 | # at 0.9875 (98.75%) 653 | quantile = 2.2414027276049473 654 | # Co-array 655 | X_var = xij 656 | # Chi^2; default is 90% confidence (p = 0.90) 657 | # Special closed form for 2 degrees of freedom 658 | chi2 = -2 * np.log(1 - p) 659 | 660 | for jj in range(0, nits): 661 | 662 | # Check for data spike: 663 | if np.count_nonzero(tau[:, jj, :]) < (co_array_num - 2): 664 | # We have a data spike, so do not process. 665 | continue 666 | 667 | # Now use original arrays 668 | y_var = tau[:, jj, :] 669 | 670 | residuals = y_var - ( 671 | X_var @ coeffs[:, jj].reshape(dimension_number, 1)) 672 | sor = np.sort(residuals.flatten()**2) 673 | s0 = np.sqrt(np.sum(sor[0:h]) / h) * raw_factor 674 | 675 | if np.abs(s0) < 1e-7: 676 | weights = (np.abs(residuals) < 1e-7) 677 | z_final = coeffs[:, jj].reshape(dimension_number, 1) 678 | else: 679 | weights = (np.abs(residuals / s0) <= quantile) 680 | weights = weights.flatten() 681 | # Cast logical to int 682 | weights_int = weights * 1 683 | 684 | # Perform the weighted least squares fit with 685 | # only data points with weight = 1 686 | # to increase statistical efficiency. 687 | q, r = np.linalg.qr(X_var[weights, :]) 688 | qt = q.conj().T @ y_var[weights] 689 | z_final = np.linalg.lstsq(r, qt)[0] 690 | 691 | # Find dropped data points 692 | # Final residuals 693 | residuals = y_var - (X_var @ z_final) 694 | weights_num = np.sum(weights_int) 695 | scale = np.sqrt(np.sum(residuals[weights]**2) / (weights_num - 1)) 696 | scale *= rew_factor1 697 | if weights_num != co_array_num: 698 | # Final fit - correction factor to make LTS approximately normally distributed # noqa 699 | rew_factor2 = rew_consfactor_lts(weights, dimension_number, co_array_num) # noqa 700 | scale *= rew_factor2 701 | weights = np.abs(residuals / scale) <= 2.5 702 | weights = weights.flatten() 703 | 704 | # Trace velocity & back-azimuth conversion 705 | # x-component of slowness vector 706 | sx = z_final[0][0] 707 | # y-component of slowness vector 708 | sy = z_final[1][0] 709 | # Calculate trace velocity from slowness 710 | lts_vel[jj] = 1/np.linalg.norm(z_final, 2) 711 | # Convert baz from mathematical CCW from E 712 | # to geographical CW from N. baz = arctan(sx/sy) 713 | lts_baz[jj] = (np.arctan2(sx, sy) * 180 / np.pi - 360) % 360 714 | 715 | # Uncertainty Quantification - Szuberla & Olson, 2004 716 | # Compute co-array eigendecomp. for uncertainty calcs. 717 | c_eig_vals, c_eig_vecs = np.linalg.eigh(xij[weights, :].T @ xij[weights, :]) 718 | eig_vec_ang = np.arctan2(c_eig_vecs[1, 0], c_eig_vecs[0, 0]) 719 | R = np.array([[np.cos(eig_vec_ang), np.sin(eig_vec_ang)], 720 | [-np.sin(eig_vec_ang), np.cos(eig_vec_ang)]]) 721 | 722 | # Calculate the sigma_tau value (Szuberla et al. 2006). 723 | residuals = tau[weights, jj, :] - (xij[weights, :] @ z_final) 724 | m_w, _ = np.shape(xij[weights, :]) 725 | with np.errstate(invalid='raise'): 726 | try: 727 | sigma_tau[jj] = np.sqrt(tau[weights, jj, :].T @ residuals / ( 728 | m_w - dimension_number))[0] 729 | except FloatingPointError: 730 | pass 731 | 732 | # Equation 16 (Szuberla & Olson, 2004) 733 | sigS = sigma_tau[jj] / np.sqrt(c_eig_vals) 734 | # Form uncertainty ellipse major/minor axes 735 | a = np.sqrt(chi2) * sigS[0] 736 | b = np.sqrt(chi2) * sigS[1] 737 | # Rotate uncertainty ellipse to align major/minor axes 738 | # along coordinate system axes 739 | So = R @ [sx, sy] 740 | # Find angle & slowness extrema 741 | try: 742 | eExtrm, eVec = rthEllipse(a, b, So[0], So[1]) 743 | except ValueError: 744 | eExtrm = np.array([np.nan, np.nan, np.nan, np.nan]) 745 | eVec = np.array([[np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan]]) 746 | # Rotate eigenvectors back to original orientation 747 | eVec = eVec @ R 748 | # Fix up angle calculations 749 | sig_theta = np.abs(np.diff( 750 | (np.arctan2(eVec[2:, 1], eVec[2:, 0]) * 180 / np.pi - 360) 751 | % 360)) 752 | if sig_theta > 180: 753 | sig_theta = np.abs(sig_theta - 360) 754 | 755 | # Halving here s.t. +/- value expresses uncertainty bounds. 756 | # Remove the 1/2's to get full values to express 757 | # coverage ellipse area. 758 | conf_int_baz[jj] = 0.5 * sig_theta 759 | conf_int_vel[jj] = 0.5 * np.abs(np.diff(1 / eExtrm[:2])) 760 | 761 | # Cast weights to int for output 762 | element_weights[:, jj] = weights * 1 763 | 764 | return lts_vel, lts_baz, element_weights, sigma_tau, conf_int_vel, conf_int_baz 765 | 766 | 767 | def array_from_weights(weightarray, idx): 768 | """ Return array element pairs from LTS weights. 769 | 770 | Args: 771 | weightarray (array): An m x 0 array of the 772 | final LTS weights for each element pair. 773 | idx (array): An m x 2 array of the element pairs; 774 | generated from the `get_cc_time` function. 775 | 776 | Returns: 777 | (array): 778 | ``fstations``: A 1 x m array of element pairs. 779 | 780 | """ 781 | 782 | a = np.where(weightarray == 0)[0] 783 | stn1, stn2 = zip(*idx) 784 | stn1 = np.array(stn1) 785 | stn2 = np.array(stn2) 786 | 787 | # Add one for plotting purposes; offset python 0-based indexing. 788 | stn1 += 1 789 | stn2 += 1 790 | 791 | # Flagged stations 792 | fstations = np.concatenate((stn1[a], stn2[a])) 793 | return fstations 794 | 795 | 796 | ################## 797 | # Class definitions 798 | ################## 799 | class LsBeam: 800 | """ Base class for least squares beamforming. This class is not meant to be used directly. """ 801 | 802 | def __init__(self, data): 803 | # 2D Beamforming (trace_velocity and back-azimuth) 804 | self.dimension_number = 2 805 | # Pre-allocate Arrays 806 | # Median of the cross-correlation maxima 807 | self.mdccm = np.full(data.nits, np.nan) 808 | # Time 809 | self.t = np.full(data.nits, np.nan) 810 | # Trace Velocity [m/s] 811 | self.lts_vel = np.full(data.nits, np.nan) 812 | # Back-azimuth [degrees] 813 | self.lts_baz = np.full(data.nits, np.nan) 814 | # Calculate co-array and indices 815 | self.calculate_co_array(data) 816 | # Co-array size is N choose 2, N = num. of array elements 817 | self.co_array_num = int((data.nchans * (data.nchans - 1)) / 2) 818 | # Pre-allocate time delays 819 | self.tau = np.empty((self.co_array_num, data.nits)) 820 | # Pre-allocate for median-absolute devation (MAD) of time delays 821 | self.time_delay_mad = np.zeros(data.nits) 822 | # Confidence interval for trace velocity 823 | self.conf_int_vel = np.full(data.nits, np.nan) 824 | # Confidence interval for back-azimuth 825 | self.conf_int_baz = np.full(data.nits, np.nan) 826 | # Pre-allocate for sigma-tau 827 | self.sigma_tau = np.full(data.nits, np.nan) 828 | # Specify station dictionary to maintain cross-compatibility 829 | self.stdict = {} 830 | # Confidence value for uncertainty calculation 831 | self.p = 0.90 832 | # Check co-array rank for least squares problem 833 | if (np.linalg.matrix_rank(self.xij) < self.dimension_number): 834 | raise RuntimeError('Co-array is ill posed for the least squares problem. Check array coordinates. xij rank < ' + str(self.dimension_number)) 835 | 836 | def calculate_co_array(self, data): 837 | """ Calculate the co-array coordinates (x, y) for the array. 838 | """ 839 | # Calculate element pair indices 840 | self.idx_pair = [(ii, jj) for ii in range(data.nchans - 1) for jj in range(ii + 1, data.nchans)] # noqa 841 | # Calculate the co-array 842 | self.xij = data.rij[:, np.array([ii[0] for ii in self.idx_pair])] - data.rij[:, np.array([jj[1] for jj in self.idx_pair])] # noqa 843 | self.xij = self.xij.T 844 | # Calculate median absolute deviation and standardized 845 | # co-array coordinates for least squares fit. 846 | self.xij_standardized = np.zeros_like(self.xij) 847 | self.xij_mad = np.zeros(2) 848 | for jj in range(0, self.dimension_number): 849 | self.xij_mad[jj] = 1.4826 * np.median(np.abs(self.xij[:, jj])) 850 | self.xij_standardized[:, jj] = self.xij[:, jj] / self.xij_mad[jj] 851 | 852 | def correlate(self, data): 853 | """ Cross correlate the time series data. 854 | """ 855 | for jj in range(0, data.nits): 856 | # Get time from middle of window, except for the end. 857 | t0_ind = data.intervals[jj] 858 | tf_ind = data.intervals[jj] + data.winlensamp 859 | try: 860 | self.t[jj] = data.tvec[t0_ind + int(data.winlensamp/2)] 861 | except: 862 | self.t[jj] = np.nanmax(self.t, axis=0) 863 | 864 | # Numba doesn't accept mode='full' in np.correlate currently 865 | # Cross correlate the wave forms. Get the differential times. 866 | # Pre-allocate the cross-correlation matrix 867 | self.cij = np.empty((data.winlensamp * 2 - 1, self.co_array_num)) 868 | for k in range(self.co_array_num): 869 | # MATLAB's xcorr w/ 'coeff' normalization: 870 | # unit auto-correlations. 871 | self.cij[:, k] = (np.correlate(data.data[t0_ind:tf_ind, self.idx_pair[k][0]], data.data[t0_ind:tf_ind, self.idx_pair[k][1]], mode='full') / np.sqrt(np.sum(data.data[t0_ind:tf_ind, self.idx_pair[k][0]] * data.data[t0_ind:tf_ind, self.idx_pair[k][0]]) * np.sum(data.data[t0_ind:tf_ind, self.idx_pair[k][1]] * data.data[t0_ind:tf_ind, self.idx_pair[k][1]]))) # noqa 872 | # Find the median of the cross-correlation maxima 873 | self.mdccm[jj] = np.nanmedian(self.cij.max(axis=0)) 874 | # Form the time delay vector and save it 875 | delay = np.argmax(self.cij, axis=0) + 1 876 | self.tau[:, jj] = (data.winlensamp - delay) / data.sampling_rate 877 | self.time_delay_mad[jj] = 1.4826 * np.median( 878 | np.abs(self.tau[:, jj])) 879 | 880 | self.tau = np.reshape(self.tau, (self.co_array_num, data.nits, 1)) 881 | 882 | 883 | class OLSEstimator(LsBeam): 884 | """ Class for ordinary least squares beamforming.""" 885 | 886 | def __init__(self, data): 887 | super().__init__(data) 888 | # Pre-compute co-array QR factorization for least squares 889 | self.q_xij, self.r_xij = np.linalg.qr(self.xij_standardized) 890 | 891 | def solve(self, data): 892 | """ Calculate trace velocity, back-azimuth, MdCCM, and confidence intervals. 893 | 894 | Args: 895 | data (DataBin): The DataBin object. 896 | """ 897 | 898 | # Pre-compute co-array eigendecomp. for uncertainty calcs. 899 | c_eig_vals, c_eig_vecs = np.linalg.eigh(self.xij.T @ self.xij) 900 | eig_vec_ang = np.arctan2(c_eig_vecs[1, 0], c_eig_vecs[0, 0]) 901 | R = np.array([[np.cos(eig_vec_ang), np.sin(eig_vec_ang)], 902 | [-np.sin(eig_vec_ang), np.cos(eig_vec_ang)]]) 903 | # Chi^2; default is 90% confidence (p = 0.90) 904 | # Special closed form for 2 degrees of freedom 905 | chi2 = -2 * np.log(1 - self.p) 906 | 907 | # Loop through time 908 | for jj in range(data.nits): 909 | 910 | # Check for data spike. 911 | if (self.time_delay_mad[jj] == 0) or (np.count_nonzero(self.tau[:, jj, :]) < (self.co_array_num - 2)): 912 | # We have a data spike, so do not process. 913 | continue 914 | 915 | y_var = self.tau[:, jj, :] / self.time_delay_mad[jj] 916 | qt = self.q_xij.conj().T @ y_var 917 | z_final = lstsq(self.r_xij, qt)[0] 918 | 919 | # Correct coefficients from standardization 920 | for ii in range(0, self.dimension_number): 921 | z_final[ii] *= self.time_delay_mad[jj] / self.xij_mad[ii] # noqa 922 | 923 | # x-component of slowness vector 924 | sx = z_final[0][0] 925 | # y-component of slowness vector 926 | sy = z_final[1][0] 927 | # Calculate trace velocity from slowness 928 | self.lts_vel[jj] = 1/np.linalg.norm(z_final, 2) 929 | # Convert baz from mathematical CCW from E 930 | # to geographical CW from N. baz = arctan(sx/sy) 931 | self.lts_baz[jj] = (np.arctan2(sx, sy) * 180 / np.pi - 360) % 360 932 | 933 | # Calculate the sigma_tau value (Szuberla et al. 2006). 934 | residuals = self.tau[:, jj, :] - (self.xij @ z_final) 935 | self.sigma_tau[jj] = np.sqrt(self.tau[:, jj, :].T @ residuals / ( 936 | self.co_array_num - self.dimension_number))[0] 937 | 938 | # Calculate uncertainties from Szuberla & Olson, 2004 939 | # Equation 16 940 | sigS = self.sigma_tau[jj] / np.sqrt(c_eig_vals) 941 | # Form uncertainty ellipse major/minor axes 942 | a = np.sqrt(chi2) * sigS[0] 943 | b = np.sqrt(chi2) * sigS[1] 944 | # Rotate uncertainty ellipse to align major/minor axes 945 | # along coordinate system axes 946 | So = R @ [sx, sy] 947 | # Find angle & slowness extrema 948 | try: 949 | # rthEllipse routine can be unstable; catch instabilities 950 | eExtrm, eVec = rthEllipse(a, b, So[0], So[1]) 951 | # Rotate eigenvectors back to original orientation 952 | eVec = eVec @ R 953 | # Fix up angle calculations 954 | sig_theta = np.abs(np.diff( 955 | (np.arctan2(eVec[2:, 1], eVec[2:, 0]) * 180 / np.pi - 360) 956 | % 360)) 957 | if sig_theta > 180: 958 | sig_theta = np.abs(sig_theta - 360) 959 | 960 | # Halving here s.t. +/- value expresses uncertainty bounds. 961 | # Remove the 1/2's to get full values to express 962 | # coverage ellipse area. 963 | self.conf_int_baz[jj] = 0.5 * sig_theta 964 | self.conf_int_vel[jj] = 0.5 * np.abs(np.diff(1 / eExtrm[:2])) 965 | 966 | except ValueError: 967 | self.conf_int_baz[jj] = np.nan 968 | self.conf_int_vel[jj] = np.nan 969 | 970 | 971 | class LTSEstimator(LsBeam): 972 | """ Class for least trimmed squares (LTS) beamforming. 973 | """ 974 | 975 | def __init__(self, data): 976 | super().__init__(data) 977 | # Pre-allocate array of slowness coefficients 978 | self.slowness_coeffs = np.empty((self.dimension_number, data.nits)) 979 | # Pre-allocate weights 980 | self.element_weights = np.zeros((self.co_array_num, data.nits)) 981 | # Raise error if a LTS object is instantiated with ALPHA = 1.0. 982 | # The ordinary least squares code should be used instead 983 | if data.alpha == 1.0: 984 | raise RuntimeError('ALPHA = 1.0. This class is computionally inefficient. Use the OLSEstimator class instead.') 985 | # Raise error if there are too few data points for subsetting 986 | if np.shape(self.xij)[0] < (2 * self.dimension_number): 987 | raise RuntimeError('The co-array must have at least 4 elements for least trimmed squares. Check rij array coordinates.') 988 | # Calculate the subset size. 989 | self.h_calc(data) 990 | # The number of subsets we will test. 991 | self.n_samples = 500 992 | # The number of best subsets to try in the final iteration. 993 | self.candidate_size = 10 994 | # The initial number of concentration steps. 995 | self.csteps = 4 996 | # The number of concentration steps for the second stage. 997 | self. csteps2 = 100 998 | 999 | def h_calc(self, data): 1000 | r""" Generate the h-value, the number of points to fit. 1001 | 1002 | Args: 1003 | ALPHA (float): The decimal percentage of points 1004 | to keep. Default is 0.75. 1005 | n (int): The total number of points. 1006 | p (int): The number of parameters. 1007 | 1008 | Returns: 1009 | (int): 1010 | ``h``: The number of points to fit. 1011 | """ 1012 | 1013 | self.h = int(np.floor(2*np.floor((self.co_array_num + self.dimension_number + 1) / 2) - self.co_array_num + 2 * (self.co_array_num - np.floor((self.co_array_num + self.dimension_number + 1) / 2)) * data.alpha)) # noqa 1014 | 1015 | def solve(self, data): 1016 | """ Apply the FAST_LTS algorithm to calculate a least trimmed squares solution for trace velocity, back-azimuth, MdCCM, and confidence intervals. 1017 | 1018 | Args: 1019 | data (DataBin): The DataBin object. 1020 | """ 1021 | # Determine the best slowness coefficients from FAST-LTS 1022 | self.slowness_coeffs = fast_LTS(data.nits, self.tau, self.time_delay_mad, self.xij_standardized, self.xij_mad, self.dimension_number, self.candidate_size, self.n_samples, self.co_array_num, self.slowness_coeffs, self.csteps, self.h, self.csteps2, random_set, check_array) # noqa 1023 | # Use the best slowness coefficients to determine dropped stations 1024 | # Calculate uncertainties at 90% confidence 1025 | self.lts_vel, self.lts_baz, self.element_weights, self.sigma_tau, self.conf_int_vel, self.conf_int_baz = post_process(self.dimension_number, self.co_array_num, data.alpha, self.h, data.nits, self.tau, self.xij, self.slowness_coeffs, self.lts_vel, self.lts_baz, self.element_weights, self.sigma_tau, self.p, self.conf_int_vel, self.conf_int_baz) # noqa 1026 | # Find dropped stations from weights 1027 | # Map dropped data points back to elements. 1028 | for jj in range(0, data.nits): 1029 | stns = array_from_weights( 1030 | self.element_weights[:, jj], self.idx_pair) 1031 | # Stash the number of elements for plotting. 1032 | if len(stns) > 0: 1033 | tval = str(self.t[jj]) 1034 | self.stdict[tval] = stns 1035 | if jj == (data.nits - 1) and data.alpha != 1.0: 1036 | self.stdict['size'] = data.nchans 1037 | -------------------------------------------------------------------------------- /lts_array/classes/lts_data_class.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from obspy.geodetics.base import calc_vincenty_inverse 4 | 5 | 6 | class DataBin: 7 | """ Data container for LTS processing 8 | 9 | Args: 10 | window_length (float): The window processing length [sec.] 11 | window_overlap (float): The decimal overalp [0.0, 1.0) for consecutive time windows. 12 | alpha (float): The decimal [0.50, 1.0] amount of data to keep in the subsets. 13 | """ 14 | 15 | def __init__(self, window_length, window_overlap, alpha): 16 | self.window_length = window_length 17 | self.window_overlap = window_overlap 18 | self.alpha = alpha 19 | 20 | def build_data_arrays(self, st, latlist, lonlist, remove_elements=None): 21 | """ Collect basic data from stream file. Project lat/lon into r_ij coordinates. 22 | 23 | Args: 24 | st (stream): An obspy stream object. 25 | latlist (list): A list of latitude points. 26 | lonlist (list): A list of longitude points. 27 | remove_elements (list): A list of elements to remove before processing. Python numbering is used, so "[0]" removes the first element. 28 | """ 29 | # Check that all traces have the same length 30 | if len(set([len(tr) for tr in st])) != 1: 31 | raise ValueError('Traces in stream must have same length!') 32 | 33 | # Remove predetermined elements before processing 34 | if (remove_elements is not None) and (len(remove_elements) > 0): 35 | remove_elements = np.sort(remove_elements) 36 | for jj in range(0, len(remove_elements)): 37 | st.remove(st[remove_elements[jj]]) 38 | latlist.remove(latlist[remove_elements[jj]]) 39 | lonlist.remove(lonlist[remove_elements[jj]]) 40 | remove_elements -= 1 41 | 42 | # Save the station name 43 | self.station_name = st[0].stats.station 44 | # Pull processing parameters from the stream file. 45 | self.nchans = len(st) 46 | self.npts = st[0].stats.npts 47 | # Save element names from location 48 | # If blank, pull from IMS-style station name 49 | self.element_names = [] 50 | for tr in st: 51 | if tr.stats.location == '': 52 | # IMS element names 53 | self.element_names.append(tr.stats.station[-2:]) 54 | else: 55 | self.element_names.append(tr.stats.location) 56 | # Assumes all traces have the same sample rate and length 57 | self.sampling_rate = st[0].stats.sampling_rate 58 | self.winlensamp = int(self.window_length * self.sampling_rate) 59 | # Sample increment (delta_t) 60 | self.sampinc = int(np.round( 61 | (1 - self.window_overlap) * self.winlensamp)) 62 | # Time intervals to window data 63 | self.intervals = np.arange(0, self.npts - self.winlensamp, self.sampinc, dtype='int') # noqa 64 | self.nits = len(self.intervals) 65 | # Pull time vector from stream object 66 | self.tvec = st[0].times('matplotlib') 67 | # Store data traces in an array for processing. 68 | self.data = np.empty((self.npts, self.nchans)) 69 | for ii, tr in enumerate(st): 70 | self.data[:, ii] = tr.data 71 | # Set the array coordinates 72 | self.rij = self.getrij(latlist, lonlist) 73 | # Make sure the least squares problem is well-posed 74 | # rij must have at least 3 elements 75 | if np.shape(self.rij)[1] < 3: 76 | raise RuntimeError('The array must have at least 3 elements for well-posed least squares estimation. Check rij array coordinates.') 77 | # Is least trimmed squares or ordinary least squares going to be used? 78 | if self.alpha == 1.0: 79 | print('ALPHA is 1.00. Performing an ordinary', 80 | 'least squares fit, NOT least trimmed squares.') 81 | 82 | def getrij(self, latlist, lonlist): 83 | r""" Calculate element locations (r_ij) from latitude and longitude. 84 | 85 | Return the projected geographic positions 86 | in X-Y (Cartesian) coordinates. Points are calculated 87 | with the Vincenty inverse and will have a zero-mean. 88 | 89 | Args: 90 | latlist (list): A list of latitude points. 91 | lonlist (list): A list of longitude points. 92 | 93 | Returns: 94 | (array): 95 | ``rij``: A numpy array with the first row corresponding to 96 | cartesian "X" - coordinates and the second row 97 | corresponding to cartesian "Y" - coordinates. 98 | 99 | """ 100 | 101 | # Check that the lat-lon arrays are the same size. 102 | if (len(latlist) != self.nchans) or (len(lonlist) != self.nchans): 103 | raise ValueError('Mismatch between the number of stream channels and the latitude or longitude list length.') # noqa 104 | 105 | # Pre-allocate "x" and "y" arrays. 106 | xnew = np.zeros((self.nchans, )) 107 | ynew = np.zeros((self.nchans, )) 108 | 109 | for jj in range(1, self.nchans): 110 | # Obspy defaults to the WGS84 ellipsoid. 111 | delta, az, _ = calc_vincenty_inverse( 112 | latlist[0], lonlist[0], latlist[jj], lonlist[jj]) 113 | # Convert azimuth to degrees from North 114 | az = (450 - az) % 360 115 | xnew[jj] = delta/1000 * np.cos(az*np.pi/180) 116 | ynew[jj] = delta/1000 * np.sin(az*np.pi/180) 117 | 118 | # Remove the mean. 119 | xnew -= np.mean(xnew) 120 | ynew -= np.mean(ynew) 121 | 122 | rij = np.array([xnew.tolist(), ynew.tolist()]) 123 | 124 | return rij 125 | 126 | def plot_array_coordinates(self): 127 | """ Plot array element locations in Cartesian coordinates to the default device. 128 | """ 129 | # Plot array coordinates 130 | fig = plt.figure(1) 131 | plt.clf() 132 | plt.plot(self.rij[0, :], self.rij[1, :], 'ro') 133 | plt.axis('equal') 134 | plt.ylabel('km') 135 | plt.xlabel('km') 136 | plt.title(self.station_name) 137 | for jj in range(0, self.nchans): 138 | plt.text(self.rij[0, jj], self.rij[1, jj], self.element_names[jj]) 139 | fig.show() 140 | -------------------------------------------------------------------------------- /lts_array/ltsva.py: -------------------------------------------------------------------------------- 1 | from lts_array.classes.lts_data_class import DataBin 2 | from lts_array.classes.lts_classes import OLSEstimator, LTSEstimator 3 | 4 | # Don't print FutureWarning for scipy.lstsq 5 | import warnings 6 | warnings.simplefilter(action='ignore', category=FutureWarning) 7 | 8 | 9 | def ltsva(st, lat_list, lon_list, window_length, window_overlap, alpha=1.0, plot_array_coordinates=False, remove_elements=None): 10 | r""" Process infrasound or seismic array data with least trimmed squares (LTS). 11 | 12 | Args: 13 | st: Obspy stream object. Assumes response has been removed. 14 | lat_list (list): List of latitude values for each element in ``st``. 15 | lon_list (list): List of longitude values for each element in ``st``. 16 | window_length (float): Window length in seconds. 17 | window_overlap (float): Window overlap in the range (0.0 - 1.0). 18 | alpha (float): Fraction of data for LTS subsetting [0.5 - 1.0]. 19 | Choose 1.0 for ordinary least squares (default). 20 | plot_array_coordinates (bool): Plot array coordinates? Defaults to False. 21 | remove_elements (list): (Optional) Remove element number(s) from ``st``, ``lat_list``, and ``lon_list`` before processing. Here numbering refers to the Python index (e.g. [0] = remove 1st element in stream). 22 | 23 | Returns: 24 | (tuple): 25 | A tuple of array processing parameters: 26 | ``lts_vel`` (array): An array of trace velocity estimates. 27 | ``lts_baz`` (array): An array of back-azimuth estimates. 28 | ``t`` (array): An array of times centered on the processing windows. 29 | ``mdccm`` (array): An array of median cross-correlation maxima. 30 | ``stdict`` (dict): A dictionary of flagged element pairs. 31 | ``sigma_tau`` (array): An array of sigma_tau values. 32 | ``conf_int_vel`` (array): An array of 95% confidence intervals for the trace velocity. 33 | ``conf_int_baz`` (array): An array of 95% confidence intervals for the back-azimuth. 34 | 35 | """ 36 | 37 | # Build data object 38 | data = DataBin(window_length, window_overlap, alpha) 39 | data.build_data_arrays(st, lat_list, lon_list, remove_elements) 40 | 41 | # Plot array coordinates as a check 42 | if plot_array_coordinates: 43 | data.plot_array_coordinates() 44 | 45 | if data.alpha == 1.0: 46 | # Ordinary Least Squares 47 | ltsva = OLSEstimator(data) 48 | else: 49 | # Least Trimmed Squares 50 | ltsva = LTSEstimator(data) 51 | ltsva.correlate(data) 52 | ltsva.solve(data) 53 | 54 | return ltsva.lts_vel, ltsva.lts_baz, ltsva.t, ltsva.mdccm, ltsva.stdict, ltsva.sigma_tau, ltsva.conf_int_vel, ltsva.conf_int_baz 55 | -------------------------------------------------------------------------------- /lts_array/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .lts_array_plot import lts_array_plot -------------------------------------------------------------------------------- /lts_array/tools/lts_array_plot.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | from copy import deepcopy 5 | from collections import Counter 6 | 7 | 8 | def lts_array_plot(st, lts_vel, lts_baz, t, mdccm, stdict=None): 9 | ''' Return a Least-trimmed squares array processing plot, including flagged element pairs. 10 | 11 | Plots first channel waveform, trace-velocity, back-azimuth, and LTS-flagged element pairs. 12 | 13 | Args: 14 | st (stream): Obspy stream. Assumes response has been removed. 15 | stdict (dict): Dictionary of flagged element pairs 16 | from the `fast_lts_array` function. 17 | t (array): Array of time values for each parameter estimate. 18 | mdccm (array): Array of median cross-correlation maximas. 19 | lts_vel (array): Array of least-trimmed squares 20 | trace velocity estimates. 21 | lts_baz (array): Array of least-trimmed squares 22 | back-azimuths estimates. 23 | 24 | Returns: 25 | (tuple): 26 | ``fig``: Output figure handle. 27 | ``axs``: Output figure axes. 28 | 29 | Example: 30 | fig, axs = lts_array_plot(st, lts_vel, lts_baz, t, mdccm, stdict) 31 | ''' 32 | 33 | # Specify the colormap. 34 | cm = 'RdYlBu_r' 35 | # Colorbar/y-axis limits for MdCCM. 36 | cax = (0.2, 1) 37 | # Specify the time vector for plotting the trace. 38 | tvec = st[0].times('matplotlib') 39 | 40 | # Check station dictionary input. It must be a non-empy dictionary. 41 | if not isinstance(stdict, dict) or not stdict: 42 | stdict = None 43 | 44 | # Determine the number and order of subplots. 45 | num_subplots = 3 46 | vplot = 1 47 | bplot = 2 48 | splot = bplot 49 | if stdict is not None: 50 | num_subplots += 1 51 | splot = bplot + 1 52 | 53 | # Start plotting. 54 | fig, axarr = plt.subplots(num_subplots, 1, sharex='col') 55 | fig.set_size_inches(9, 12) 56 | axs = axarr.ravel() 57 | axs[0].plot(tvec, st[0].data, 'k') 58 | axs[0].axis('tight') 59 | axs[0].set_ylabel('Pressure [Pa]') 60 | axs[0].text(0.15, 0.93, st[0].stats.station, horizontalalignment='center', 61 | verticalalignment='center', transform=axs[0].transAxes) 62 | cbaxes = fig.add_axes( 63 | [0.95, axs[splot].get_position().y0, 0.02, 64 | axs[vplot].get_position().y1 - axs[splot].get_position().y0]) 65 | 66 | # Plot the trace velocity plot. 67 | sc = axs[vplot].scatter(t, lts_vel, c=mdccm, 68 | edgecolors='k', lw=0.1, cmap=cm) 69 | axs[vplot].set_ylim(0.25, 0.45) 70 | axs[vplot].set_xlim(t[0], t[-1]) 71 | sc.set_clim(cax) 72 | axs[vplot].set_ylabel('Trace Velocity\n [km/s]') 73 | 74 | # Plot the back-azimuth estimates. 75 | sc = axs[bplot].scatter(t, lts_baz, c=mdccm, 76 | edgecolors='k', lw=0.1, cmap=cm) 77 | axs[bplot].set_ylim(0, 360) 78 | axs[bplot].set_xlim(t[0], t[-1]) 79 | sc.set_clim(cax) 80 | axs[bplot].set_ylabel('Back-azimuth\n [deg]') 81 | 82 | hc = plt.colorbar(sc, cax=cbaxes, ax=[axs[1], axs[2]]) 83 | hc.set_label('MdCCM') 84 | 85 | # Plot dropped station pairs from LTS if given. 86 | if stdict is not None: 87 | ndict = deepcopy(stdict) 88 | n = ndict['size'] 89 | ndict.pop('size', None) 90 | tstamps = list(ndict.keys()) 91 | tstampsfloat = [float(ii) for ii in tstamps] 92 | 93 | # Set the second colormap for station pairs. 94 | cm2 = plt.get_cmap('binary', (n-1)) 95 | initplot = np.empty(len(t)) 96 | initplot.fill(1) 97 | 98 | axs[splot].scatter(np.array([t[0], t[-1]]), 99 | np.array([0.01, 0.01]), c='w') 100 | axs[splot].axis('tight') 101 | axs[splot].set_ylabel('Element [#]') 102 | axs[splot].set_xlabel('UTC Time') 103 | axs[splot].set_xlim(t[0], t[-1]) 104 | axs[splot].set_ylim(0.5, n+0.5) 105 | axs[splot].xaxis_date() 106 | axs[splot].tick_params(axis='x', labelbottom='on') 107 | 108 | # Loop through the stdict for each flag and plot 109 | for jj in range(len(tstamps)): 110 | z = Counter(list(ndict[tstamps[jj]])) 111 | keys, vals = z.keys(), z.values() 112 | keys, vals = np.array(list(keys)), np.array(list(vals)) 113 | pts = np.tile(tstampsfloat[jj], len(keys)) 114 | sc2 = axs[splot].scatter(pts, keys, c=vals, edgecolors='k', 115 | lw=0.1, cmap=cm2, vmin=0.5, vmax=n-0.5) 116 | 117 | # Add the horizontal colorbar for station pairs. 118 | p3 = axs[splot].get_position().get_points().flatten() 119 | p3 = axs[splot].get_position() 120 | cbaxes2 = fig.add_axes([p3.x0, p3.y0-0.08, p3.width, 0.02]) 121 | hc2 = plt.colorbar(sc2, orientation="horizontal", 122 | cax=cbaxes2, ax=axs[splot]) 123 | hc2.set_label('Number of Flagged Element Pairs') 124 | 125 | axs[splot].xaxis_date() 126 | axs[splot].set_xlabel('UTC Time') 127 | 128 | return fig, axs 129 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | install: 5 | - requirements: docs/requirements.txt 6 | - method: setuptools 7 | path: . 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os as os 3 | 4 | # https://github.com/readthedocs/readthedocs.org/issues/5512#issuecomment-475073310 5 | on_rtd = os.environ.get('READTHEDOCS') == 'True' 6 | if on_rtd: 7 | INSTALL_REQUIRES = [] 8 | else: 9 | INSTALL_REQUIRES = ['matplotlib', 'numpy', 'obspy', 'scipy', 'numba'] 10 | 11 | setup( 12 | name='lts_array', 13 | version='2.0', 14 | description='Apply least trimmed squares to infrasound and seismic array processing.', 15 | license='LICENSE.txt', 16 | author='Jordan W. Bishop', 17 | url="https://github.com/uafgeotools/lts_array", 18 | packages=find_packages(), 19 | python_requires='>=3.0', 20 | install_requires=INSTALL_REQUIRES, 21 | scripts=[ 22 | 'example.py', 23 | ] 24 | ) 25 | --------------------------------------------------------------------------------