Source code for watex.externals.zutils

#!/usr/bin/env python
#       Authors: Lars Krieger, Jared R. Peacock & Alison Louise Kirkby
#       License: GPL
#       Edited by LKouadio 
# Original file can be found in  < https://github.com/MTgeophysics/mtpy.git>

import numpy as np
import math, cmath
from ..exceptions import EMError 

#define uncertainty for differences between time steps
epsilon = 1e-9
#magnetic permeability in free space in H/m (=Vs/Am)
mu0 = 4e-7*math.pi

[docs] def centre_point(xarray, yarray): """ get the centre point of arrays of x and y values """ return (xarray.max() + xarray.min())/2., (yarray.max() + yarray.min())/2.
[docs] def roundsf(number, sf): """ round a number to a specified number of significant figures (sf) """ # can't have < 1 s.f. sf = max(sf,1.) rounding = int(np.ceil(-np.log10(number) + sf - 1.)) return np.round(number, rounding)
[docs] def get_period_list(period_min,period_max,periods_per_decade,include_outside_range=True): """ get a list of values (e.g. periods), evenly spaced in log space and including values on multiples of 10 :returns: numpy array containing list of values :inputs: period_min = minimum period period_max = maximum period periods_per_decade = number of periods per decade include_outside_range = option whether to start and finish the period list just inside or just outside the bounds specified by period_min and period_max default True """ log_period_min = np.log10(period_min) log_period_max = np.log10(period_max) # check if log_period_min is a whole number if log_period_min % 1 > 0: # list of periods, around the minimum period, that will be present in specified # periods per decade aligned_logperiods_min = np.linspace(np.floor(log_period_min), np.ceil(log_period_min),periods_per_decade + 1) lpmin_diff = log_period_min - aligned_logperiods_min # index of starting period, smallest value > 0 if include_outside_range: spimin = np.where(lpmin_diff > 0)[0][-1] else: spimin = np.where(lpmin_diff < 0)[0][0] start_period = aligned_logperiods_min[spimin] else: start_period = log_period_min if log_period_max % 1 > 0: # list of periods, around the maximum period, that will be present in specified # periods per decade aligned_logperiods_max = np.linspace(np.floor(log_period_max), np.ceil(log_period_max),periods_per_decade + 1) lpmax_diff = log_period_max - aligned_logperiods_max # index of starting period, smallest value > 0 if include_outside_range: spimax = np.where(lpmax_diff < 0)[0][0] else: spimax = np.where(lpmax_diff > 0)[0][-1] stop_period = aligned_logperiods_max[spimax] else: stop_period = log_period_max return np.logspace(start_period,stop_period, int((stop_period-start_period)*periods_per_decade + 1))
[docs] def nearest_index(val,array): """ find the index of the nearest value in the array :param val: the value to search for :param array: the array to search in :return: index: integer describing position of nearest value in array """ # absolute difference between value and array diff = np.abs(array-val) return np.where(diff==min(diff))[0][0]
[docs] def make_log_increasing_array(z1_layer, target_depth, n_layers, increment_factor=0.999): """ create depth array with log increasing cells, down to target depth, inputs are z1_layer thickness, target depth, number of layers (n_layers) """ # make initial guess for maximum cell thickness max_cell_thickness = target_depth # make initial guess for log_z log_z = np.logspace(np.log10(z1_layer), np.log10(max_cell_thickness), num=n_layers) counter = 0 while np.sum(log_z) > target_depth: max_cell_thickness *= increment_factor log_z = np.logspace(np.log10(z1_layer), np.log10(max_cell_thickness), num=n_layers) counter += 1 if counter > 1e6: break return log_z
[docs] def invertmatrix_incl_errors(inmatrix, inmatrix_err=None): """ Invert matrices with inclinaison errors""" if inmatrix is None: raise EMError('Matrix must be defined') if (inmatrix_err is not None) and (inmatrix.shape != inmatrix_err.shape): raise EMError('Matrix and err-matrix shapes do not match: %s - %s'%(str(inmatrix.shape), str(inmatrix_err.shape))) if (inmatrix.shape[-2] != inmatrix.shape[-1]): raise EMError('Matrices must be square!') if (inmatrix_err is not None) and (inmatrix_err.shape[-2] != inmatrix_err.shape[-1]) : raise EMError('Matrices must be square!') dim = inmatrix.shape[-1] det = np.linalg.det(inmatrix) if det == 0: raise EMError('Matrix is singular - I cannot invert that!') inv_matrix = np.zeros_like(inmatrix) if dim != 2: raise EMError('Only 2D matrices supported yet') inv_matrix = np.linalg.inv(inmatrix) inv_matrix_err = None if (inmatrix_err is not None): inmatrix_err = np.real(inmatrix_err) inv_matrix_err = np.zeros_like(inmatrix_err) for i in range(2): for j in range(2): #looping over the entries of the error matrix err = 0. for k in range(2): for l in range(2): #each entry has 4 summands err += np.abs (- inv_matrix[i,k] * inv_matrix[l,j] * inmatrix_err[k,l]) inv_matrix_err[i,j] = err return inv_matrix, inv_matrix_err
[docs] def rhophi2z(rho, phi, freq): """ Convert impedance-style information given in Rho/Phi format into complex valued Z. Input: rho - 2x2 array (real) - in Ohm m phi - 2x2 array (real) - in degrees freq - scalar - frequency in Hz Output: Z - 2x2 array (complex) """ try: if rho.shape != (2,2) or phi.shape != (2,2): raise if not (rho.dtype in ['float', 'int'] and phi.dtype in ['float', 'int']): raise except: raise EMError('ERROR - arguments must be two 2x2 arrays (real)') z = np.zeros((2,2),'complex') for i in range(2): for j in range(2): abs_z = np.sqrt(5 * freq * rho[i,j]) z[i,j] = cmath.rect(abs_z ,math.radians(phi[i,j])) return z
[docs] def compute_determinant_error(z_array, z_err_array, method = 'theoretical', repeats=1000): """ compute the error of the determinant of z using a stochastic method seed random z arrays with a normal distribution around the input array :param z_array: z (impedance) array containing real and imaginary values :param z_err_array: impedance error array containing real values, in MT we assume the real and imag errors are the same :param method: method to use, theoretical calculation or stochastic :return: error: array of real values with same shape as z_err_array representing the error in the determinant of Z :return: error_sqrt: array of real values with same shape as z_err_array representing the error in the (determinant of Z)**0.5 """ if method == 'stochatic': arraylist = [] for r in range(repeats): errmag = np.random.normal(loc=0,scale=z_err_array,size=z_array.shape) arraylist = np.append(arraylist,z_array + errmag*(1. + 1j)) arraylist = arraylist.reshape(repeats,z_array.shape[0],2,2) detlist = np.linalg.det(arraylist) error = np.std(detlist,axis=0) else: error = np.abs(z_err_array[:,0,0]*np.abs(z_array[:,1,1]) + z_err_array[:,1,1]*np.abs(z_array[:,0,0]) \ - z_err_array[:,0,1]*np.abs(z_array[:,1,0]) - z_err_array[:,1,0]*np.abs(z_array[:,0,1])) return error
[docs] def propagate_error_polar2rect(r,r_error,phi, phi_error): """ Find error estimations for the transformation from polar to cartesian coordinates. Uncertainties in polar representation define a section of an annulus. Find the 4 corners of this section and additionally the outer boundary point, which is defined by phi = phi0, rho = rho0 + sigma rho. The cartesian "box" defining the uncertainties in x,y is the outer bound around the annulus section, defined by the four outermost points. So check the four corners as well as the outer boundary edge of the section to find the extrema in x znd y. These give you the sigma_x/y. """ corners = [ ( np.real(cmath.rect(r-r_error, phi-phi_error)), np.imag(cmath.rect(r-r_error, phi-phi_error))),\ ( np.real(cmath.rect(r+r_error, phi-phi_error)), np.imag(cmath.rect(r+r_error, phi-phi_error))),\ ( np.real(cmath.rect(r+r_error, phi+phi_error)), np.imag(cmath.rect(r+r_error, phi+phi_error))),\ ( np.real(cmath.rect(r-r_error, phi+phi_error)), np.imag(cmath.rect(r-r_error, phi+phi_error))),\ ( np.real(cmath.rect(r+r_error, phi)), np.imag(cmath.rect(r+r_error, phi))) ] lo_x = [i[0] for i in corners] lo_y = [i[1] for i in corners] point = (np.real(cmath.rect(r, phi)), np.imag(cmath.rect(r, phi)) ) lo_xdiffs = [ abs(point[0] - i) for i in lo_x] lo_ydiffs = [ abs(point[1] - i) for i in lo_y] xerr = max(lo_xdiffs) yerr = max(lo_ydiffs) return xerr, yerr
[docs] def propagate_error_rect2polar(x,x_error,y, y_error): """Find error estimations for the transformation from cartesian coordinates to polar. """ # x_error, y_error define a rectangular uncertainty box # rho error is the difference between the closest and furthest point of the box (w.r.t. the origin) # approximation: just take corners and midpoint of edges lo_points = [ (x + x_error, y), (x - x_error, y), (x, y - y_error ), (x, y + y_error ),\ (x - x_error, y - y_error) ,(x + x_error, y - y_error) ,(x + x_error, y + y_error) ,(x - x_error, y + y_error) ] #check, if origin is within the box: origin_in_box = False if x_error >= np.abs(x) and y_error >= np.abs(y): origin_in_box = True lo_polar_points = [ cmath.polar(np.complex(*i)) for i in lo_points ] lo_rho = [i[0] for i in lo_polar_points ] lo_phi = [math.degrees(i[1])%360 for i in lo_polar_points ] rho_err = 0.5*(max(lo_rho) - min(lo_rho) ) phi_err = 0.5*(max(lo_phi) - min(lo_phi)) if (270 < max(lo_phi) < 360 ) and (0 < min(lo_phi) < 90 ): tmp1 = [ i for i in lo_phi if (0 < i < 90) ] tmp4 = [ i for i in lo_phi if (270 < i < 360) ] phi_err = 0.5*((max(tmp1) - min(tmp4))%360) if phi_err > 180: #print phi_err,' -> ',(-phi_err)%360 phi_err = (-phi_err)%360 if origin_in_box is True: #largest rho: rho_err = 2*rho_err + min(lo_rho) #maximum angle uncertainty: phi_err = 180. return rho_err, phi_err
[docs] def z_error2r_phi_error(z_real, z_imag, error): """ Error estimation from rectangular to polar coordinates. By standard error propagation, relative error in resistivity is 2*relative error in z amplitude. Uncertainty in phase (in degrees) is computed by defining a circle around the z vector in the complex plane. The uncertainty is the absolute angle between the vector to (x,y) and the vector between the origin and the tangent to the circle. :returns: tuple containing relative error in resistivity, absolute error in phase :inputs: z_real = real component of z (real number or array) z_imag = imaginary component of z (real number or array) error = absolute error in z (real number or array) """ z_amp = np.abs(z_real + 1j*z_imag) with np.errstate(all='ignore'): z_rel_err = error/z_amp res_rel_err = 2.*z_rel_err #if the relative error of the amplitude is >=100% that means that the relative #error of the resistivity is 200% - that is then equivalent to an uncertainty #in the phase angle of 90 degrees: if np.iterable(z_real): phi_err = np.degrees(np.arctan(z_rel_err)) phi_err[res_rel_err > 1.] = 90. else: if res_rel_err > 1.: phi_err = 90 else: phi_err = np.degrees(np.arctan(z_rel_err)) return res_rel_err, phi_err
[docs] def old_z_error2r_phi_error(x,x_error,y, y_error): """ Error estimation from rect to polar, but with small variation needed for MT: the so called 'relative phase error' is NOT the relative phase error, but the ABSOLUTE uncertainty in the angle that corresponds to the relative error in the amplitude. So, here we calculate the transformation from rect to polar coordinates, esp. the absolute/length of the value. Then we find the uncertainty in this length and calculate the relative error of this. The relative error of the resistivity will be double this value, because it's calculated by taking the square of this length. The relative uncertainty in length defines a circle around (x,y) (APPROXIMATION!). The uncertainty in phi is now the absolute of the angle beween the vector to (x,y) and the origin-vector tangential to the circle. BUT....since the phase angle uncertainty is interpreted with regard to the resistivity and not the Z-amplitude, we have to look at the square of the length, i.e. the relative error in question has to be halfed to get the correct relationship between resistivity and phase errors!!. """ # x_error, y_error define a rectangular uncertainty box # rho error is the difference between the closest and furthest point of the box (w.r.t. the origin) # approximation: just take corners and midpoint of edges lo_points = [ (x + x_error, y), (x - x_error, y), (x, y - y_error ), (x, y + y_error ), (x - x_error, y - y_error) , (x + x_error, y - y_error) ,(x + x_error, y + y_error) , (x - x_error, y + y_error) ] #check, if origin is within the box: origin_in_box = False if x_error >= np.abs(x) and y_error >= np.abs(y): origin_in_box = True lo_polar_points = [ cmath.polar(np.complex(*i)) for i in lo_points ] lo_rho = [i[0] for i in lo_polar_points ] lo_phi = [math.degrees(i[1])%360 for i in lo_polar_points ] #uncertainty in amplitude is defined by half the diameter of the box around x,y rho_err = 0.5*(max(lo_rho) - min(lo_rho) ) rho = cmath.polar(np.complex(x,y))[0] try: rel_error_rho = rho_err/rho except: rel_error_rho = 0. #if the relative error of the amplitude is >=100% that means that the relative #error of the resistivity is 200% - that is then equivalent to an uncertainty #in the phase angle of 90 degrees: if rel_error_rho > 1.: phi_err = 90 else: phi_err = np.degrees(np.arcsin( rel_error_rho)) return rho_err, phi_err
#rotation: #1. rotation positive in clockwise direction #2. orientation of new X-axis X' given by rotation angle #3. express contents of Z/tipper (points P) in this new system (points P') #4. rotation for points calculated as P' = ([cos , sin ],[-sin, cos]) * P <=> P' = R * P #5. => B' = R * B and E' = R * E # (Rt is the inverse rotation matrix) #6. E = Z * B => Rt * E' = Z * Rt * B' => E' = (R*Z*Rt) * B' => Z' = (R*Z*Rt) #7. Bz = T * B => Bz = T * Rt * B' => T' = (T * Rt) #(Since Tipper is a row vector) #7.a for general column vectors v: v' = R * v # Rotation of the uncertainties: # a) rotate Z into Z' # b) use propagation of errors on Z' to obtain the rotated Z'err # That is NOT the same as the rotated error matrix Zerr (although the result is similar)
[docs] def rotatematrix_incl_errors(inmatrix, angle, inmatrix_err = None) : """ rotates the matrix including the propagation errors.""" if inmatrix is None : raise EMError('Matrix AND eror matrix must be defined') if (inmatrix_err is not None) and (inmatrix.shape != inmatrix_err.shape): raise EMError('Matrix and err-matrix shapes do not match: %s - %s'%(str(inmatrix.shape), str(inmatrix_err.shape))) try: degreeangle = angle % 360 except: raise EMError('"Angle" must be a valid number (in degrees)') phi = math.radians(degreeangle) cphi = np.cos(phi) sphi = np.sin(phi) # JP: Changed the rotation matrix to be formulated to rotate # counter clockwise, I cannot find a good reason for this except that # when you plot the strike and phase tensors the look correct with this # formulation. rotmat = np.array([[ cphi, sphi], [-sphi, cphi]]) # rotmat = np.array([[ cphi, -sphi], [sphi, cphi]]) rotated_matrix = np.dot(np.dot(rotmat, inmatrix), np.linalg.inv(rotmat)) errmat = None if (inmatrix_err is not None) : err_orig = np.real(inmatrix_err) errmat = np.zeros_like(inmatrix_err) # standard propagation of errors: errmat[0,0] = np.sqrt( (cphi**2 * err_orig[0,0])**2 + \ (cphi * sphi * err_orig[0,1])**2 + \ (cphi * sphi * err_orig[1,0])**2 + \ (sphi**2 * err_orig[1,1])**2 ) errmat[0,1] = np.sqrt( (cphi**2 * err_orig[0,1])**2 + \ (cphi * sphi * err_orig[1,1])**2 + \ (cphi * sphi * err_orig[0,0])**2 + \ (sphi**2 * err_orig[1,0])**2 ) errmat[1,0] = np.sqrt( (cphi**2 * err_orig[1,0])**2 + \ (cphi * sphi * err_orig[1,1])**2 +\ (cphi * sphi * err_orig[0,0])**2 + \ (sphi**2 * err_orig[0,1])**2 ) errmat[1,1] = np.sqrt( (cphi**2 * err_orig[1,1])**2 + \ (cphi * sphi * err_orig[0,1])**2 + \ (cphi * sphi * err_orig[1,0])**2 + \ (sphi**2 * err_orig[0,0])**2 ) return rotated_matrix, errmat
[docs] def rotatevector_incl_errors(invector, angle, invector_err = None): """ rotates vector including the propagation errors.""" #check for row or column vector if invector is None : raise EMError('Vector AND error-vector must be defined') if (invector_err is not None) and (invector.shape != invector_err.shape): raise EMError('Vector and errror-vector shapes do not match: %s - %s'%(str(invector.shape), str(invector_err.shape))) try: degreeangle = angle%360 except: raise EMError('"Angle" must be a valid number (in degrees)') phi = math.radians(degreeangle) cphi = np.cos(phi) sphi = np.sin(phi) # JP: Changed the rotation matrix to be formulated to rotate # counter clockwise, I cannot find a good reason for this except that # when you plot the strike and phase tensors the look correct with this # formulation. rotmat = np.array([[ cphi, sphi],[-sphi, cphi]]) # rotmat = np.array([[ cphi, -sphi],[sphi, cphi]]) if invector.shape == (1, 2): rotated_vector = np.dot( invector, np.linalg.inv(rotmat) ) else: rotated_vector = np.dot( rotmat, invector ) errvec = None if (invector_err is not None) : errvec = np.zeros_like(invector_err) if invector_err.shape == (1, 2): errvec = np.dot(invector_err, np.abs(np.linalg.inv(rotmat))) else: errvec = np.dot(np.abs(rotmat), invector_err ) return rotated_vector, errvec
[docs] def multiplymatrices_incl_errors(inmatrix1, inmatrix2, inmatrix1_err = None,inmatrix2_err = None ): """ multiplies matrices including the propagation errors.""" if inmatrix1 is None or inmatrix2 is None: raise EMError('ERROR - two 2x2 arrays needed as input') if inmatrix1.shape != inmatrix2.shape: raise EMError('ERROR - two 2x2 arrays with same dimensions needed as input') # np.matrix should be deprecated soon so consider replacing # by np.ndarray prod = np.array(np.dot( np.array(inmatrix1), np.array(inmatrix2))) if (inmatrix1_err is None) or ( inmatrix1_err is None ): return prod, None var = np.zeros((2,2)) var[0,0] = (inmatrix1_err[0,0] * inmatrix2[0,0])**2 + (inmatrix1_err[0,1] * inmatrix2[1,0])**2+\ (inmatrix2_err[0,0] * inmatrix1[0,0])**2 + (inmatrix2_err[1,0] * inmatrix1[0,1])**2 var[0,1] = (inmatrix1_err[0,0] * inmatrix2[0,1])**2 + (inmatrix1_err[0,1] * inmatrix2[1,1])**2+\ (inmatrix2_err[0,1] * inmatrix1[0,0])**2 + (inmatrix2_err[1,1] * inmatrix1[0,1])**2 var[1,0] = (inmatrix1_err[1,0] * inmatrix2[0,0])**2 + (inmatrix1_err[1,1] * inmatrix2[1,0])**2+\ (inmatrix2_err[0,0] * inmatrix1[1,0])**2 + (inmatrix2_err[1,0] * inmatrix1[1,1])**2 var[1,1] = (inmatrix1_err[1,0] * inmatrix2[0,1])**2 + (inmatrix1_err[1,1] * inmatrix2[1,1])**2+\ (inmatrix2_err[0,1] * inmatrix1[1,0])**2 + (inmatrix2_err[1,1] * inmatrix1[1,1])**2 return prod, np.sqrt(var)
[docs] def reorient_data2D(x_values, y_values, x_sensor_angle = 0 , y_sensor_angle = 90): """ Re-orient time series data of a sensor pair, which has not been in default (x=0, y=90) orientation. Input: - x-values - Numpy array - y-values - Numpy array Note: same length for both! - If not, the shorter length is taken Optional: - Angle of the x-sensor - measured in degrees, clockwise from North (0) - Angle of the y-sensor - measured in degrees, clockwise from North (0) Output: - corrected x-values (North) - corrected y-values (East) """ x_values = np.array(x_values) y_values = np.array(y_values) try: if x_values.dtype not in ['complex', 'float', 'int']: raise if len(x_values) != len(y_values): raise except: raise EMError('ERROR - both input arrays must be of same length') if len(x_values) != len(y_values): l = min(len(x_values) , len(y_values)) x_values = x_values[:l] y_values = y_values[:l] in_array = np.zeros((len(x_values), 2), x_values.dtype) in_array[:,0] = x_values in_array[:,1] = y_values try: x_angle = math.radians(x_sensor_angle) y_angle = math.radians(y_sensor_angle) except: raise EMError('ERROR - both angles must be of type int or float') T = np.array( [[ np.real(cmath.rect(1,x_angle)), np.imag(cmath.rect(1,x_angle))],[np.real(cmath.rect(1,y_angle)), np.imag(cmath.rect(1,y_angle))]]) try: new_array = np.dot(in_array, np.linalg.inv(T) ) # T.I except: raise EMError('ERROR - angles must define independent axes to span 2D') #print new_array.shape return new_array[:,0], new_array[:,1]