import numpy as np # from copy import deepcopy from matplotlib import cm # from matplotlib.colors import ListedColormap as LCM from scipy.interpolate import interp1d import matplotlib.pyplot as plt ''' Objects for interfacing with Michael Porter's models. Designed in analog with the types of objects called by read_shd, write_fieldflp, write_env, etc. ''' class Source: def __init__(self, depth): self.depth = depth self.x = None self.y = None class Dom: def __init__(self, ran, depth, offsets=None): self.range = ran # in km self.depth = depth if offsets is not None: self.offsets = offsets class Pos: def __init__(self, Source, Dom): self.s = Source self.r = Dom class Ice: def __init__(self, BumDen, eta, xi): self.BumDen= BumDen self.eta = eta self.xi = xi class SSPraw: def __init__(self,z, alphaR, betaR, rho, alphaI, betaI): """ Input: z - numpy 1d array Depths the ssp is taken at alphaR - numpy 1d array ssp vals betaR - numpy 1d array shear speeds rho - numpy 1d array density vals at the points alphaI - numpy 1d array attenuation (p-wave) betaI - numpy 1d array shear attenuation """ self.z = z self.alphaR = alphaR # sound speed in array self.betaR = betaR # shear wave speed self.rho = rho self.alphaI = alphaI # atten. in array (alpha (z)) self.betaI = betaI # shear attenuation def make_sspf(self): self.sspf = interp1d(self.z, self.alphaR) def interp_all(self): self.betaI_f = interp1d(self.z, self.betaI) self.betaR_f = interp1d(self.z, self.betaR) self.rho_f = interp1d(self.z, self.rho) self.alphaR_f = interp1d(self.z, self.alphaR) self.alphaI_f = interp1d(self.z, self.alphaI) return self.alphaR_f, self.betaR_f, self.rho_f, self.alphaI_f, self.betaI_f class SSP: def __init__(self, raw, depth, NMedia, Opt=None, N=None, sigma=0, ranges=0): self.NMedia = NMedia # number of media layers self.Opt = Opt # option: not sure what this is for self.N = N # array with num points in each layer, don't include one for halfpace self.sigma = sigma # ''interfacial roughness'', one for each layer? see Kuperman and ingenito 1977 for definition, include a point for the halfspace self.depth = depth # depth array for layers self.ranges = ranges self.raw = raw # list of raw ssp profile self.sspf = None def make_sspf(self): for raw in self.raw: raw.make_sspf() if self.NMedia > 1: self.ssp_vals = np.zeros((len(self.depth), 1)) if self.NMedia == 2: layer_depth = self.raw[0].z[-1] self.sspf = lambda z: np.piecewise(z, [z=layer_depth], [lambda z: self.raw[0].sspf(z), lambda z: self.raw[1].sspf(z)]) elif self.NMedia == 3: layer_depth_one = self.raw[0].z[-1] layer_depth_two = self.raw[1].z[-1] func1 = lambda z: np.piecewise(z, [z= layer_depth_one], [lambda z: self.raw[0].sspf(z), lambda z: self.raw[1].sspf(z)]) self.sspf = lambda z: np.piecewise(z, [z= layer_depth_two], [lambda z: func1(z), lambda z: self.raw[2].sspf(z)]) else: raise ValueError("too many layers") else: self.ssp_vals = self.raw[0].alphaR self.sspf = self.raw[0].sspf class HS: def __init__(self, alphaR=np.array([]), betaR=np.array([]), rho=np.array([]), alphaI=np.array([]), betaI=np.array([])): self.alphaR = np.array(alphaR) self.betaR = np.array(betaR) self.rho = np.array(rho) self.alphaI = np.array(alphaI) self.betaI = np.array(betaI) class BotBndry: def __init__(self, Opt, Hs, depth=[], betaI=[0], ): self.Opt = Opt # 'A' for analytic or 'CVW' for interpolated ssp self.hs = Hs class TopBndry: def __init__(self, Opt, depth=[]): self.Opt = Opt self.cp = None self.cs = None self.rho = None class Bndry: def __init__(self, top, bot): self.Top = top self.Bot = bot class Box: def __init__(self, z, r): self.r = r self.z = z class Beam: def __init__(self, RunType=None, Type=None,Nbeams=None, Ibeam=None, Nrays=None, alpha=None, deltas=None, box=None, epmult=None, rloop=None, Ibwin=None, Nimage = None): self.RunType = RunType self.Type = Type self.Nbeams = Nbeams self.Ibeam = Ibeam self.Nrays = Nrays self.alpha = alpha self.deltas = deltas self.box = box self.epmult = epmult self.rloop = rloop self.Ibwin = Ibwin self.Nimage = None class cInt: def __init__(self, low, high): self.Low = low self.High = high class Field: def __init__(self, **kwargs): self.pos = kwargs['pos'] self.greens = kwargs['pressure'] self.freq = kwargs['freq'] # a list of frequencies def contour(self): shape = np.shape(self.greens) if len(shape) > 2: raise ValueError("Field object is too high dimensional, try multi_contour") pos = self.pos ax = plt.contourf(pos.r.range, -pos.r.depth, np.log10(abs(self.greens.real))) return ax def multi_contour(self): print('not implemented, maybe one day...') class Modes: def __init__(self, **kwargs): self.M = kwargs['M'] self.k = kwargs['modes_k'] self.z = np.array(kwargs['z']) self.phi = kwargs['modes_phi'] self.top = kwargs['top'] self.bot = kwargs['bot'] self.N = kwargs['N'] self.Nfreq = kwargs['Nfreq'] self.Nmedia = kwargs['Nmedia'] self.depth = kwargs['depth'] self.rho = kwargs['rho'] self.freqvec = kwargs['freqVec'] self.init_dict = kwargs self.num_modes = self.M # easier to remember def get_excited_modes(self, sd, threshold): ''' return an array of modes that are excited by a source at sd meters depth threshold also populate some structures in moded caled excited_phi and excited_k ''' if sd not in self.z: raise ValueError("sd not in the depth array, are you sure it's the right depth you're passing?") depth_ind = [i for i in range(len(self.z)) if self.z[i] == sd][0] vals = self.phi[depth_ind,:] const = np.max(abs(vals)) filtered_inds = [i for i in range(len(self.k)) if abs(self.phi[depth_ind, i]) / const > threshold] self.excited_phi = self.phi[:, filtered_inds] self.excited_k = self.k[filtered_inds] return self.excited_phi, self.excited_k def get_source_depth_ind(self, sd): """ sd is an int """ tol = 1e-2 sind = [i for i in range(len(self.z)) if abs(self.z[i]-sd) < tol] if len(sind) == 0 : raise ValueError("sd not in the depth array, are you sure it's the right depth you're passing?") else: self.sind = sind[0] return self.sind def remove_source_pos(self, sd): """ Take the source at sd from the mode matrix Initiate a new attribute to hold the source modal value """ sind = self.get_source_depth_ind(sd) new_pos_len = len(self.z) - 1 new_phi = np.zeros((new_pos_len, self.num_modes), dtype=self.phi.dtype) new_phi[:sind, :] = self.phi[:sind, :] new_phi[sind:,:] = self.phi[sind+1:,:] self.phi = new_phi new_z = np.zeros((new_pos_len), dtype=self.z.dtype) new_z[:sind] = self.z[:sind] new_z[sind:] = self.z[sind+1:] self.z = new_z self.source_strength = self.phi[sind,:] return def get_source_strength(self, sd): """ Get the value of each mode at the source depth sd (meters) Initialize new attribute for the source strength """ sind = self.get_source_depth_ind(sd) vals = self.phi[sind,:] self.source_strength = vals return vals def get_receiver_modes(self, zr): """ zr is array like """ tol = 1e-3 r_inds = [i for i in range(len(self.z)) if np.min(abs(self.z[i]-zr)) < tol] receiver_modes = self.phi[r_inds, :] self.receiver_modes = receiver_modes return receiver_modes def get_source_excitation(self, zs): """ For case where there is a track, there may be multiple repeats in zs """ tol = 1e-3 r_inds = [np.argmin(abs(zs[i] - self.z)) for i in range(len(zs))] strength_modes = self.phi[r_inds, :] self.strength_modes = strength_modes return strength_modes def plot(self): figs = [] if self.M > 5: for i in range(self.M): fig = plt.figure(i) plt.plot(self.phi[:,i], -self.z) figs.append(fig) return figs def __repr__(self): return 'Modes object with ' + str(self.M) + ' distinct modes' class Arrival: def __init__(self, info_list): self.amp = info_list[0] self.delay = info_list[1] self.src_ang = info_list[2] self.rec_ang = info_list[3] self.num_top_bnc = info_list[4] self.num_bot_bnc = info_list[5] class Arrivals: def __init__(self, arrival_list): self.arrivals = arrival_list def plot_cir(self, vals=None,ax=None): """ Plot the channel impulse response Vals provides the option to pass in a set of arrivals to use fo calibrating the y axis If vals=None, I use the arrivals in the object to calibrate the y-axis I don't create a figure I return ymin, ymax, vals so that the values from this arrival set can be used in a more global scope to set the y-axis limits (useful for creating videos comprised of snapshots of a bunch of CIR's). For example, I want to plot a bunch of Arrivals on the same plot, or combine a bunch of CIRs into a video. Then I need to have the same axis limits for all the plots. I can use default vals (None) for the first plot, then use the ymin, ymax, vals return values in a larger scope to standardize the axes. Then for the second, third, ... plots, I pass in vals to make sure they all have the same time axis. Output - ymin - float ymax -float plus/minus 1.5 times max amplitude value vals - list vals[0] is the times vals[1] is the amplitude vals """ arrival_list = self.arrivals amps = np.array([abs(x.amp) for x in arrival_list]) phase = np.array([np.angle(x.amp) for x in arrival_list]) times = np.array([x.delay for x in arrival_list]) """ Create the time axis """ if type(vals) != type(None): t = np.linspacea(.9*np.min(vals[0]), 1.1*np.max(vals[0]), 100) else: t = np.linspace(.9*np.min(times), 1.1*np.max(times), 100) zeros = np.zeros(t.size) if type(ax) == type(None): fig, ax = plt.subplots(2,1) ax.scatter(t.real, zeros, s=6) ax.stem(times.real, amps.real, markerfmt=' ', basefmt=' ',use_line_collection=True) ax.scatter(times.real, amps,s=16,c='k') scale = 1.5*np.max(abs(amps)) # set yaxis scale ymin = - scale ymax = scale vals = [times.real, amps] return ymin, ymax, vals def plot_ellipse(self, vals=None, ref_amp =None, src_ang=False, ax=None): """ Produce the travel time ellipse with colorbar for amplitude Input - self - contains all the arrival info vals - optional allows me to use a reference arrival set to calibrate the plot axes vals[0] = times vals[0] = amps """ arrival_list = self.arrivals amps = np.array([abs(x.amp) for x in arrival_list]) times = np.array([x.delay.real for x in arrival_list]) rec_angles = np.array([x.rec_ang for x in arrival_list]) src_angles = np.array([x.rec_ang for x in arrival_list]) """ Create the time axis """ if type(vals) != type(None): tmin, tmax = np.min(vals[0]), np.max(vals[0]) else: tmin, tmax = np.min(times), np.max(times) if type(ref_amp) == type(None): amps = amps / np.max(abs(amps)) else: amps /= abs(ref_amp) cvals = 10*np.log10(abs(amps)) #cvals = np.linspace(-5, 0, len(amps)) """ Make a nice cmap """ cmap = cm.get_cmap('Spectral') """ Configure the x axis """ if type(ax) == type(None): fig, ax = plt.subplots(2,1) ax.scatter([tmin, tmax], [0,0], s=0) if src_ang == False: angles = rec_angles else: angles = src_angles max_db_down = 10 cvals_normed = abs(cvals) / max_db_down strong_inds = cvals > -max_db_down ax.scatter(times[strong_inds], angles[strong_inds], s=25, c=cmap(cvals_normed[strong_inds])) vals = [times, amps] ymin, ymax = -1.5*np.max(abs(angles)), 1.5*np.max(abs(angles)) return ymin, ymax, vals class KernInput: def __init__(self, Field_r, Field_s, env): self.gr = Field_r.greens_mat self.num_rcvrs = len(self.gr) self.gs = np.flip(Field_s.greens_mat, axis=1) # move source off axis self.env = env self.Pos = self.Field_r.Pos class Eigenray: def __init__(self, launch_ang, num_top_bnc, num_bot_bnc, xy_arr): """ launch_ang launch angl in degrees num_top_bnc integer num_bot_bnc integer xy_arr - np array 2 by num steps, first is x position, second is y (so plt.plot(xy_arr[0,:], xy_arr[1,:]) will plot the ray) """ self.src_ang = launch_ang self.num_top_bnc = num_top_bnc self.num_bot_bnc = num_bot_bnc self.xy = xy_arr