#!/usr/bin/env python
"""This script/module contains routines that are used to analyze/visualize the data sets
in the standard AVNI format."""
#####################  IMPORT STANDARD MODULES   ######################################
# python 3 compatibility
from __future__ import absolute_import, division, print_function
import sys
if (sys.version_info[:2] < (3, 0)):
    from builtins import float,int,list
import numpy as np #for numerical analysis
import fortranformat as ff #reading/writing fortran formatted text
from six import string_types # to check if variable is string using isinstance
from numpy.lib.recfunctions import append_fields # for appending named columns to named numpy arrays
from scipy.interpolate import griddata
import copy
from collections import Counter
import traceback
import pandas as pd
import pint
import re
import ntpath
import uuid
import contextlib
import warnings
import os
if sys.version_info[0] >= 3: unicode = str
####################### IMPORT AVNI LIBRARIES  #######################################
from .. import constants
from .. import tools
from avni.f2py import getbullen
#######################################################################################
# 1D model class
[docs]class Reference1D(object):
    '''
    A class for 1D reference Earth models used in tomography
    '''
    #########################       magic       ##########################
    def __init__(self,file=None):
        self.data = None
        self.metadata = {}
        # assume that information about the native parameterization is not available
        # this is typical for a card deck file
        for field in ['ref_period','parameters']: self.metadata[field] = None
        self._name = None
        self._radius_max = None
        self._nlayers = None
        if file is not None:
            self.read(file)
            self.derive()
    def __str__(self):
        if self.data is not None and self._nlayers > 0:
            output = "%s is a one-dimensional model with %s layers and radius up to %s km" % (self._name, self._nlayers,self._radius_max/1000.)
        else:
            output = "No model has been read into this reference1D instance yet"
        return output
    def __repr__(self):
        return '{self.__class__.__name__}({self._name})'.format(self=self)
    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result
    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, copy.deepcopy(v, memo))
        return result
    def __add__(self, other):
        raise NotImplementedError('method to add 1D instances on top of each other')
    #########################       decorators       ##########################
    @property
    def name(self):
        return self._name
    #########################       methods       #############################
[docs]    def derive(self):
        if self.data is not None and self._nlayers > 0:
            self.get_Love_elastic()
            self.get_discontinuity() 
[docs]    def read(self,file):
        '''
        Read a card deck file used in OBANI. Other formats not ready yet
        '''
        try:
            self.read_mineos_cards(file)
        except:
            var1 = traceback.format_exc()
            try:
                self.read_bases_coefficients(file)
            except:
                var2 = traceback.format_exc()
                print('############    Tried reading as mineos cards   ############')
                print(var1)
                print('############    Tried reading as bases coefficients   ############')
                print(traceback.format_exc())
                raise NotImplementedError('model format is not currently implemented in reference1D.read') 
[docs]    def plot(self):
        from ..plots import plotreference1d
        plotreference1d(self) 
[docs]    def read_bases_coefficients(self,file):
        # Use tools.eval_polynomial and tools.eval_splrem
        coef_names=['bottom','top','spline','constant','linear','quadratic','cubic']
        # regex dict for common metadata info
        rx_dict_common = {
            'model': re.compile(r'EARTH MODEL\s*:\s*(?P<model>.*)\n'),
            'ref_period': re.compile(r'REFERENCE PERIOD\s*:\s*(?P<ref_period>.*)\n'),
            'norm_radius': re.compile(r'NORMALIZING RADIUS\s*:\s*(?P<norm_radius>.*)\n'),
            'parameters': re.compile(r'#PARAMETERS\s*:\s*(?P<parameters>.*)\n'),
            'num_region': re.compile(r'NUMBER OF REGIONS\s*:\s*(?P<num_region>.*)\n'),
            'regions': re.compile(r'REGION\s*:\s*(?P<regions>.*)\n'),
            'units': re.compile(r'#UNITS\s*:\s*(?P<units>.*)\n'),
        }
        # Check if it is the first file read for the model
        if self._name == None:
            # make parameterization 2D lists of dicts,first dimension associated with file
            # number, here we assume the parameterization within each file are the same
            self.metadata['parameterization'] = [[]]
            # make parameters list of dicts, PARAMETERS should not be the same
            self.metadata['attributes'] = {}
            regions = []
            # loop through lines in file, extract info
            with open(file,'r') as f:
                line = f.readline()
                while line:
                # at each line check for a match with a regex
                    key, match = tools.parse_line(line,rx_dict_common)
                    if key == 'model':
                        self._name = match.group('model')
                    if key == 'ref_period':
                        ref_temp = match.group('ref_period')
                        self.metadata['ref_period'] = float(ref_temp)
                    if key == 'norm_radius':
                        rad_temp = match.group('norm_radius')
                        self.metadata['norm_radius'] = float(rad_temp)
                        if self.metadata['norm_radius'] != constants.R.to('km').magnitude:
                            raise AssertionError('norm_radius '+str(self.metadata['norm_radius'])+' is consistent with avni constant '+str(constants.R.to('km').magnitude)+'. Reinitialize avni with tools.common.getplanetconstants.')
                    if key == 'parameters':
                        para_list = match.group('parameters').lower().split()
                        self.metadata['parameters']=para_list
                    if key == 'units':
                        unit_list = match.group('units').split()
                        self.metadata['units']=unit_list
                    if key == 'num_region':
                        nr_temp = match.group('num_region')
                        num_region = int(nr_temp)
                    if key == 'regions':
                        regions.append(match.group('regions').strip().lower())
                    line = f.readline()
        else:
            # append a new parameterization
            self.metadata['parameterization'].append([])
            regions = []
            with open(file,'r') as f:
                line = f.readline()
                while line:
                # at each line check for a match with a regex
                    key, match = tools.parse_line(line,rx_dict_common)
                    # Check if model name is the same
                    if key == 'model':
                        if self._name != match.group('model'):
                            raise ValueError('model names should match between input files')
                    if key == 'ref_period':
                        ref_temp2 = match.group('ref_period')
                        if self.metadata['ref_period'] != float(ref_temp2):
                            print('reference period not consistent!')
                    if key == 'norm_radius':
                        rad_temp2 = match.group('norm_radius')
                        if self.metadata['norm_radius'] != float(rad_temp2):
                            print('normalizing period not consistent!')
                        if self.metadata['norm_radius'] != constants.R.to('km').magnitude:
                            raise AssertionError('norm_radius '+str(self.metadata['norm_radius'])+' is consistent with avni constant '+str(constants.R.to('km').magnitude)+'. Reinitialize avni with tools.common.getplanetconstants.')
                    if key == 'parameters':
                        para_list = match.group('parameters').lower().split()
                        for para in para_list:
                            self.metadata['parameters'].append(para)
                    if key == 'units':
                        unit_list = match.group('units').split()
                        for unit in unit_list:
                            self.metadata['units'].append(unit)
                    if key == 'num_region':
                        nr_temp = match.group('num_region')
                        num_region = int(nr_temp)
                    if key == 'regions':
                        regions.append(match.group('regions').strip().lower())
                    line = f.readline()
        # Now start read parameterization from the file
        rx_dict_para = {
            'bot_dep': re.compile(r'BOT DEPTH\s*:\s*(?P<bot_dep>.*)\n'),
            'top_dep': re.compile(r'TOP DEPTH\s*:\s*(?P<top_dep>.*)\n'),
            'bot_rad': re.compile(r'BOT RADIUS\s*:\s*(?P<bot_rad>.*)\n'),
            'top_rad': re.compile(r'TOP RADIUS*:\s*(?P<top_rad>.*)\n'),
            'lvl': re.compile(r'LEVELS\s*:\s*(?P<lvl>.*)\n'),
            'poly': re.compile(r'POLYNOMIAL\s*:\s*(?P<poly>.*)\n'),
            'spln': re.compile(r'SPLINEPNTS\s*:\s*(?P<spln>.*)\n'),
        }
        bot_deps = []
        top_deps = []
        bot_rads = []
        top_rads = []
        levels = []
        polys = []
        #radius_flag = 0 #The depth info could be saved as radius or depth, use this to mark
        with open(file,'r') as f:
            line = f.readline()
            while line:
            # at each line check for a match with a regex
                key, match = tools.parse_line(line,rx_dict_para)
                if key == 'poly':
                    polys.append(match.group('poly'))
                if key == 'spln':
                    polys.append('SPLINEPNTS : ' + match.group('spln'))
                if key == 'lvl':
                    levels.append(int(match.group('lvl')))
                if key == 'bot_dep':
                    bd_temp=match.group('bot_dep')
                    bot_rads.append(self.metadata['norm_radius']-float(bd_temp))
                if key == 'top_dep':
                    td_temp=match.group('top_dep')
                    top_rads.append(self.metadata['norm_radius']-float(td_temp))
                if key == 'bot_rad':
                    br_temp=match.group('bot_rad')
                    bot_rads.append(float(br_temp))
                    radius_flag = 1
                if key == 'top_rad':
                    tr_temp=match.group('top_rad')
                    top_rads.append(float(tr_temp))
                line = f.readline()
            bot_rads = np.array(bot_rads)
            top_rads = np.array(top_rads)
        # assign the num of regions to the n th parameterization
        iparam = len(self.metadata['parameterization']) - 1
        self.metadata['parameterization'][iparam] = {'num_regions':num_region,'filename':file,'description':'read from '+file}
        for idx,(region,poly,level,bot_rad,top_rad) in enumerate(zip(regions,polys,levels,bot_rads,top_rads)):
            self.metadata['parameterization'][iparam].update({region:{'polynomial':poly,'levels':level,'top_radius':top_rad,'bottom_radius':bot_rad}})
        # Now start to read parameter coefficient from the file
        # regex for model coefficient info
        rx_dict_coef = {
            'regions': re.compile(r'REGION\s*:\s*(?P<regions>.*)\n'),
            'poly': re.compile(r'(POLYNOMIAL|SPLINEPNTS)\s*:\s*(?P<poly>.*)\n'),
            'comment':re.compile(r'#--*\n')
        }
        for para in para_list:
            self.metadata['attributes'].update({para:{'param_index':iparam}})
        with open(file,'r') as f:
            line = f.readline()
            while line:
            # at each line check for a match with a regex
                key, match = tools.parse_line(line,rx_dict_coef)
                if key == 'regions':
                    reg_temp = (match.group('regions').strip().lower())
                    for para in para_list: self.metadata['attributes'][para].update({reg_temp:{}})
                if key == 'poly':
                    line=f.readline()
                    while line:
                        key, match = tools.parse_line(line,rx_dict_coef)
                        if key == 'comment': break
                        att,coef = line.split(":",1)
                        coef = np.array(coef.split())
                        for idx, para in enumerate(para_list):
                            self.metadata['attributes'][para][reg_temp].update({att.strip():coef[idx].astype(float)})
                        line = f.readline()
                line = f.readline()
        self.metadata['filename'] = file 
[docs]    def coefficients_to_cards(self, base_units = True):
        """evaluates bases coefficients at prescribed depth levels to get a card deck file
        base_units: convert from native units to base units in constants
        """
        import pint_pandas
        pint_pandas.PintType.ureg = constants.ureg
        # make an array of radii based on the first paramaterization that is read
        param_indx = 0
        radii = np.array([],dtype=float)
        for region in self.metadata['parameterization'][param_indx]:
            if region not in ['num_regions','filename','description']:
                top_temp = self.metadata['parameterization'][param_indx][region]['top_radius']
                bot_temp = self.metadata['parameterization'][param_indx][region]['bottom_radius']
                lvl_temp = self.metadata['parameterization'][param_indx][region]['levels']
                rad_temp = np.linspace(top_temp,bot_temp,num=lvl_temp)
                radii=np.append(radii,rad_temp)
        radii.sort() # sort from center to surface
        #names=['rho','vpv','vsv','qkappa','qmu','vph','vsh','eta']
        #use units from the elas/anelas file
        names = tools.convert2nparray(self.metadata['parameters'])
        units = tools.convert2nparray(self.metadata['units'])
        fields=list(zip(names,units))
        # loop over names and call evaluate_at_depth
        # Create data array for converted to Panda array with units
        PA_ = pint_pandas.PintArray; temp_dict = {}; temp_dict['radius'] = PA_(radii, dtype="pint[km]")
        #self.evaluate_at_depth(24.4,'vsv')
        for paraindx,param in enumerate(names):
            val_temp = self.evaluate_at_depth(self.metadata['norm_radius']-radii,param)
            # overwrite the repeated radii at bottom
            bottom_indx = np.where(np.ediff1d(radii)==0)[0]
            val_temp2 = self.evaluate_at_depth(self.metadata['norm_radius']-radii[bottom_indx],param,boundary='-')
            for indx,val in enumerate(val_temp2): val_temp[bottom_indx[indx]] = val
            temp_dict[param] = PA_(val_temp, dtype="pint["+units[paraindx]+"]")
            # convert from fractions to absolute parameter (qkappa, qmu)
            if '/' in param: # convert from fractions such as 1000/qmu to qmu
                frac = param.split('/')
                numerator = float(frac[0])
                param = frac[-1]
                val_temp = np.divide(numerator,val_temp,out=np.zeros_like(val_temp), where=val_temp!=0)
            # loop over names and check if there's /name; modify units if needed
                temp_dict[param] = PA_(val_temp, dtype="pint[1/"+units[paraindx]+"]")
        modelarr = pd.DataFrame(temp_dict)
        if base_units: # convert to base units
            for col in modelarr.columns: modelarr[col] = modelarr[col].pint.to_base_units()
        modelarr['depth'] = PA_((constants.R.magnitude - modelarr['radius'].pint.to(constants.R.units).data).tolist(), dtype = constants.R.units)
        self._nlayers = len(modelarr['radius'])
        self.data = modelarr
        self._radius_max = max(self.data['radius']) 
[docs]    def write_mineos_cards(self,file):
        import pint_pandas
        if self.data is None or self._nlayers == 0: raise ValueError('reference1D data arrays are not allocated')
        names=['radius','rho','vpv','vsv','qkappa','qmu','vph','vsh','eta']
        units =['m','kg/m^3','m/s','m/s','dimensionless','dimensionless','m/s','m/s','dimensionless']
        # check if the units are the same or conversion is needed and where
        convert_columns = []
        for indx,name in enumerate(names):
            if pint_pandas.PintType.ureg.parse_expression(units[indx]).units != self.data[name].pint.units: convert_columns.append(name)
        disc = self.metadata['discontinuities']
        # first write the header
        printstr  =  [unicode(self._name+"\n")]
        printstr.append(unicode("1 %.1f 1 1\n" % (self.metadata['ref_period'])))
        printstr.append(unicode("  %d  %d  %d  %d  %d\n" % (self._nlayers,disc['itopic'],disc['itopoc'],disc['itopmantle'],disc['itopcrust'])))
        shape = self.data[names].shape
        output = np.zeros(shape)
        for ii in range(shape[0]):
            for jj in range(shape[1]):
                if units[jj] not in convert_columns:
                    output[ii,jj] = self.data[names].iloc[ii,jj].to(units[jj]).magnitude
                else:
                    output[ii,jj] = self.data[names].iloc[ii,jj].magnitude
        # write the string in the fortran format
        header_line  =  ff.FortranRecordWriter('f8.0,3f9.2,2f9.1,2f9.2,f9.5')
        for ii in range(shape[0]): printstr.append(unicode(header_line.write(output[ii,:])+'\n'))
        printstr[-1] = printstr[-1].split('\n')[0]
        # write the file
        f= open(file,"w")
        f.writelines(printstr)
        f.close()
        return 
[docs]    def read_mineos_cards(self,file,header = 3):
        # Operations between PintArrays of different unit registry will not work.
        # We can change the unit registry that will be used in creating new
        # PintArrays to prevent this issue.
        import pint_pandas
        pint_pandas.PintType.ureg = constants.ureg
        names=['radius','rho','vpv','vsv','qkappa','qmu','vph','vsh','eta']
        units =['m','kg/m^3','m/s','m/s','dimensionless','dimensionless','m/s','m/s','dimensionless']
        fields=list(zip(names,units))
        #formats=[float for ii in range(len(fields))]
        # modelarr = np.genfromtxt(file,dtype=None,comments='#',skip_header=3,names=fields)
        modelarr = pd.read_csv(file,skiprows=header,comment='#',sep='\s+',names=fields)
        # read the punit units from last header
        modelarr_ = modelarr.pint.quantify(level=-1)
        # Create data array
        PA_ = pint_pandas.PintArray
        modelarr_['depth'] = constants.R - modelarr_['radius'].pint.to(constants.R.units)
        self.data = modelarr_
        self._radius_max = max(self.data['radius'])
        self.metadata['parameters'] = names[1:]
        self.metadata['units'] = units[1:]
        # Get the other metadata from the first 3 line header
        with open(file,'r') as f:
            head = [next(f).strip('\n') for x in range(header)]
        self._name = head[0].strip()
        self.metadata['ref_period'] = float(head[1].split()[1])
        self.metadata['norm_radius'] = constants.R.to('km').magnitude
        # No original metadata found
        self.metadata['attributes'] = None
        # store rest of the metadata
        self.metadata['description'] = 'Read from '+file
        self.metadata['filename'] = file
        self._nlayers = len(modelarr['radius']) 
[docs]    def get_Love_elastic(self):
        '''
        Get the Love parameters and Voigt averaged elastic properties with depth
        A,C,N,L,F: anisotropy elastic Love parameters
        kappa: bulk modulus
        mu: shear modulus
        vphi: bulk sound velocity
        xi: shear anisotropy ratio
        phi: P anisotropy ratio
        Zs, Zp: S and P impedances
        '''
        if self.data is None or self._nlayers == 0: raise ValueError('reference1D data arrays are not allocated')
        # Add data fields
        self.data['a'] = self.data['rho']*self.data['vph']**2
        self.data['c'] = self.data['rho']*self.data['vpv']**2
        self.data['n'] = self.data['rho']*self.data['vsh']**2
        self.data['l'] = self.data['rho']*self.data['vsv']**2
        self.data['f'] = self.data['eta']*(self.data['a']-2.*self.data['l'])
        # equivalent isotropic
        self.data['kappa'] = (4.0*(self.data['a']+self.data['f']-self.data['n'])+self.data['c'])/9.
        self.data['mu'] = (self.data['a']+self.data['c']-2.*self.data['f']+5.*self.data['n']+6.*self.data['l'])/15.
        self.data['vp'] = ((self.data['kappa']+4.*self.data['mu']/3.)/self.data['rho']).pow(0.5)
        self.data['vs'] = (self.data['mu']/self.data['rho']).pow(0.5)
        self.data['vphi'] = (self.data['kappa']/self.data['rho']).pow(0.5)
        # anisotropy
        self.data['xi'] = (self.data['vsh'].div(self.data['vsv'])).pow(2)
        self.data['phi'] = (self.data['vpv'].div(self.data['vph'])).pow(2)
        # impedance contrasts
        self.data['Zp'] = self.data['vp']*self.data['rho']
        self.data['Zs'] = self.data['vs']*self.data['rho']
        # Add metadata
        for field in ['a','c','n','l','f','vp','vs','vphi','xi','phi','Zp', 'Zs','kappa','mu']:
            self.metadata['parameters'].append(field)
            self.metadata['units'].append(str(self.data[field].pint.units)) 
[docs]    def get_mineralogical(self):
        '''
        Get the Love parameters and Voigt averaged elastic properties with depth
        gravity: gavity at each depth
        Brunt-Vaisala Frequency: Used for Bullen's parameter
        Bullen: Bullen's parameter
        pressure: pressure at each depth
        poisson: Poisson's ratio
        '''
        if self.data is None or self._nlayers == 0: raise ValueError('reference1D data arrays are not allocated')
        # Operations between PintArrays of different unit registry will not work.
        # We can change the unit registry that will be used in creating new
        # PintArrays to prevent this issue.
        import pint_pandas
        pint_pandas.PintType.ureg = constants.ureg
        if constants.planetpreferred == 'Earth':
            file = tools.get_filedir()+'/'+self._name+'.'+str(uuid.uuid4())
            # write a temporary cards file
            self.write_mineos_cards(file)
            layers = self._nlayers
            grav,vaisala,bullen,pressure = getbullen(file,layers,constants.omega.to_base_units().magnitude,constants.G.to_base_units().magnitude)
            # Add data fields
            PA_ = pint_pandas.PintArray
            self.data['gravity'] = PA_(grav, dtype="pint[m/s^2]")
            self.data['Brunt-Vaisala'] = PA_(vaisala, dtype="pint[Hz]")
            self.data['Bullen'] = PA_(bullen, dtype="pint[dimensionless]")
            self.data['pressure'] = PA_(pressure, dtype="pint[Pa]")
            # Add metadata
            for field in ['gravity','Brunt-Vaisala','Bullen','pressure']:
                self.metadata['parameters'].append(field)
                self.metadata['units'].append(str(self.data[field].pint.units))
            # Poisson ratio
            vsbyvp1d = np.divide(self.data['vs'].values.quantity.magnitude, self.data['vp'].values.quantity.magnitude,out=np.zeros(self._nlayers))
            poisson = np.divide((1.-(2.*vsbyvp1d*vsbyvp1d)),(2.-(2.*vsbyvp1d*vsbyvp1d)))
            self.data['poisson'] = PA_(poisson, dtype="pint[dimensionless]")
            self.metadata['parameters'].append('poisson')
            self.metadata['units'].append('dimensionless')
            # delete a file
            with contextlib.suppress(FileNotFoundError): os.remove(file)
        else:
            warnings.warn(' mineralogical parameters not evaluated for '+constants.planetpreferred) 
[docs]    def if_discontinuity(self,depth_in_km):
        """
        Returns whether a depth is a discontinuity.
        """
        depth_in_km = tools.convert2nparray(depth_in_km)
        disc_array = self.metadata['discontinuities']['delta']['depth'].pint.to('km').values.quantity.magnitude
        output = np.zeros_like(depth_in_km,dtype=bool)
        for idep,depth in enumerate(depth_in_km):
            output[idep] = np.any(np.isclose(disc_array,depth))
        return output if len(output)>1 else output[0] 
[docs]    def get_discontinuity(self):
        '''
        Get values, average values and contrasts at discontinuities
        Output:
        ------
        Returns a structure self.metadata['disc'] that has three arrays:
        delta: containing absolute difference in parameters between smaller/larger radii
        average: containing absolute average parameters between smaller/larger radii
        contrasts: containing contrast in parameters (in %)
        '''
        if self.data is None or self._nlayers == 0: raise ValueError('reference1D data arrays are not allocated')
        disc_depths = [item.magnitude for item, count in Counter(self.data['depth']).items() if count > 1]
        disc = {}
# Create a named array for discontinuities
        for field in ['delta','average','contrast']: disc[field] = 0. * self.data.copy().drop(range(len(np.unique(disc_depths)),len(self.data)))
        # convert units to percent in contrast
        for param in self.metadata['parameters']:
            disc['contrast'][param] = (0.*disc['contrast'][param]/disc['contrast'][param][0]).pint.to('percent')
        # default names and units as percent
        names = self.data.columns.tolist()
        units = ['percent' for name in names]
        fields=list(zip(names,units))
        for icount,depth in enumerate(disc_depths):
            depth_ = constants.ureg.Quantity(depth,self.data['depth'].pint.units)
            sel = self.data[self.data['depth']==depth_]
            for field in sel:
                if field == 'radius' or field == 'depth':
                    disc['delta'][field][icount] = sel[field].iat[0]
                    disc['average'][field][icount] = sel[field].iat[0]
                    disc['contrast'][field][icount] = sel[field].iat[0]
                else:
                    disc['delta'][field][icount] = sel[field].iat[0]-sel[field].iat[1]
                    disc['average'][field][icount] = 0.5*(sel[field].iat[0]+sel[field].iat[1])
                    ## contrasts need to be in %
                    disc['contrast'][field][icount] = (abs(disc['delta'][field][icount]) / disc['average'][field][icount]).to('percent')
        #---- try to find discontinuities
        discradii = disc['delta']['radius'].pint.to('km').values.quantity.magnitude
        vp = self.data['vp'].pint.to('km/s').values.quantity.magnitude
        vs = self.data['vs'].pint.to('km/s').values.quantity.magnitude
        radii = self.data['radius'].pint.to('km').values.quantity.magnitude
        discfind = disc['delta']['radius'][np.abs(1221.5-discradii)<25.].pint.to('km').values.quantity.magnitude
        if len(discfind) <= 0: # not found
            warnings.warn(" itopic not found")
        elif len(discfind) > 1: raise ValueError('get_discontinuity: multiple values within discontinuity limits')
        else:
            disc['itopic'] = np.where(radii==discfind[0])[0][1]
        discfind = disc['delta']['radius'][np.abs(3480.0-discradii)<25.].pint.to('km').values.quantity.magnitude
        if len(discfind) <= 0: # not found
            warnings.warn(" itopoc not found")
        elif len(discfind) > 1:
            raise ValueError('get_discontinuity: multiple values within discontinuity limits')
        else:
            disc['itopoc'] = np.where(radii == discfind[0])[0][1]
        ###   Top of crust
        discfind = np.where(np.logical_and(vp < 7.5,vs > 0.))[0]
        if len(discfind) > 0: disc['itopcrust'] = max(discfind) + 1
        #discfind = disc['delta']['radius'][np.abs(6368.0-disc['delta']['radius']/1000.)<0.1]
#         if len(discfind) <= 0: # not found
#             warnings.warn(" itopcrust not found")
#         elif len(discfind) > 1:
#             raise ValueError('get_discontinuity: multiple values within discontinuity limits')
#         else:
            #disc['itopcrust'] = np.where(self.data['radius']==discfind[0])[0][1]
        itopmantle = min(np.where(vp < 7.5)[0])
        if itopmantle >0: disc['itopmantle'] = itopmantle
        self.metadata['discontinuities'] = disc 
[docs]    def get_custom_parameter(self,parameters):
        '''
        Get the arrays of custom parameters defined in various Earth models
        '''
        import pint_pandas
        if self.data is not None and self._nlayers > 0:
            PA_ = pint_pandas.PintArray
            # convert to array for ease of looping
            if isinstance(parameters,string_types): parameters = np.array([parameters])
            for ii in np.arange(parameters.size):
                if parameters[ii] not in self.metadata['parameters']:
                    if 'as' in parameters[ii]:
                        # Add data fields
                        self.data[parameters[ii]] = PA_(np.divide(self.data['vsh'].values.quantity.magnitude - self.data['vsv'].values.quantity.magnitude,self.data['vs'].values.quantity.magnitude,out=np.zeros(self._nlayers), where= self.data['vs'] != 0.)*100., dtype="pint[percent]")
                        self.metadata['parameters'].append(parameters[ii])
                        self.metadata['units'].append('percent')
                    elif 'ap' in parameters[ii]:
                        self.data[parameters[ii]] = PA_(np.divide(self.data['vph'].values.quantity.magnitude - self.data['vpv'].values.quantity.magnitude,self.data['vp'].values.quantity.magnitude,out=np.zeros(self._nlayers), where= self.data['vp'] != 0.)*100., dtype="pint[percent]")
                        self.metadata['parameters'].append(parameters[ii])
                        self.metadata['units'].append('percent')
                    elif 'vs' in parameters[ii]:
                        self.data[parameters[ii]] = self.data['vs']
                        self.metadata['parameters'].append(parameters[ii])
                        self.metadata['units'].append('m/s')
                    elif 'vp' in parameters[ii]:
                        self.data[parameters[ii]] = self.data['vp']
                        self.metadata['parameters'].append(parameters[ii])
                        self.metadata['units'].append('m/s')
                    elif 'rho' in parameters[ii]:
                        self.data[parameters[ii]] = self.data['rho']
                        self.metadata['parameters'].append(parameters[ii])
                        self.metadata['units'].append('kg/m^3')
                    else:
                        raise NotImplementedError('parameter ',parameters[ii],' is not currently implemented in reference1D.get_custom_parameter')
        else:
            raise ValueError('reference1D object is not allocated') 
[docs]    def evaluate_at_depth(self,depth_in_km,parameter='vsh',boundary='+',interpolation='linear'):
        '''
        Get the values of a parameter at a given depth
        boundary: + for value at larger radius at a discontinuity
        '''
        # need to update so it can deal with vectors
        if boundary not in ['+','-']: raise ValueError('boundary needs to be - or +')
        depth_in_km = tools.convert2nparray(depth_in_km)
        values = np.zeros_like(depth_in_km,dtype=float)
        parameters = tools.convert2nparray(self.metadata['parameters'])
        findindx = np.where(parameters == parameter)[0]
        if len(findindx) == 0 : raise KeyError('parameter '+parameter+' cannot be evaluated from reference1d instance')
        findparam = findindx.item()
        unit = self.metadata['units'][findparam]
        uniqueregions = {}
        target_region = np.empty_like(depth_in_km,dtype="U40"); target_region[:] = ''
        # detailed information about the native parameterization which went into the
        # inversion is available
        if self.metadata['attributes'] is not None:
        # check if norm_radius is within reasonable range
            if not 0.98*constants.R.to('km').magnitude <= self.metadata['norm_radius'] <= 1.02*constants.R.to('km').magnitude :
                raise ValueError('Normalizing radius not compatible')
            radius_in_km = self.metadata['norm_radius'] - depth_in_km
            param_indx = self.metadata['attributes'][parameter]['param_index']
            # finding target region in depth
            for region in self.metadata['parameterization'][param_indx]:
                if region not in ['num_regions','filename','description']:
                    # difference with top and bottom radii
                    difftop = self.metadata['parameterization'][param_indx][region]['top_radius'] - radius_in_km
                    diffbot = self.metadata['parameterization'][param_indx][region]['bottom_radius']-radius_in_km
                    dep_flag = difftop*diffbot
                    # within a region if dep_flag is neative
                    flag_array = (dep_flag < 0)
                    # if it is 0 then choose the flag based on boundary
                    findzeroindx = np.where(dep_flag==0.)[0]
                    if len(findzeroindx) != 0:
                        for indx in findzeroindx: #depth corresponds to the bottom of a region
                            if depth_in_km[indx] == 0: # surface of the solid earth
                                flag_array[indx] = True
                            else:
                                if diffbot[indx] == 0 and boundary == '+': flag_array[indx] = True
                                if difftop[indx] == 0 and boundary == '-': flag_array[indx] = True
                    for idx,flag in enumerate(flag_array):
                        if flag:
                            target_region[idx] = region
                            # store unique regions
                            if region not in [*uniqueregions]:
                                uniqueregions[region] = {'radius_range':
                                [self.metadata['parameterization'][param_indx][region]['bottom_radius'],
                                self.metadata['parameterization'][param_indx][region]['top_radius']],
                                'types': [*self.metadata['attributes'][parameter][region]]}
            if np.any(target_region == ''): raise ValueError('target regions not found')
            # create arguments for bases evaluations
            rnorm = self.metadata['norm_radius']
            #loop over all regions that are relevant to these depths
            for region  in [*uniqueregions]:
                # find all depth queries that are in this region
                indx = np.where(target_region==region)[0]
                radial_splines = False # assume that no splines are included
                choices = ['TOP', 'BOTTOM', 'CONSTANT', 'LINEAR', 'QUADRATIC', 'CUBIC']
                if not np.all([key in choices for key in uniqueregions[region]['types']]): radial_splines = True
                if not radial_splines:
                    # evaluate value of the polynomial coefficients and derivative at each depth
                    vercof,dvercof = tools.bases.eval_polynomial(radius_in_km[indx],
                            uniqueregions[region]['radius_range'] ,rnorm,
                            uniqueregions[region]['types'])
                else:
                    spline_config = self.metadata['parameterization'][param_indx][region]['polynomial'].split(':')
                    if spline_config[0].strip() != 'SPLINEPNTS':
                        raise AssertionError('Polynomial type should be SPLINEPNTS in ' + region)
                    nsplines = int(spline_config[-1])
                    vercof1,dvercof1 = tools.bases.eval_splrem(radius_in_km[indx], uniqueregions[region]['radius_range'], nsplines)
                    # in case there's only one depth, make sure the shape of vercof1 fit vercof
                    vercof1 = vercof1.reshape([len(indx),nsplines])
                    dvercof1 = dvercof1.reshape([len(indx),nsplines])
                    # select the relevant splines
                    splindx = []; nonspltype = []; isspl = np.zeros(len(uniqueregions[region]['types']),dtype='bool')
                    for typekey,spltype in enumerate(uniqueregions[region]['types']):
                        if spltype.startswith('SPLINE'):
                            splindx.append(int(spltype.split()[-1])-1)
                            isspl[typekey] = True
                        else:
                            nonspltype.append(spltype)
                    if np.all(isspl):
                        vercof = vercof1; dvercof = dvercof1
                    else:
                        # evaluate other non-spline bases
                        # doesnt seem correct
                        vercof2,dvercof2 = tools.bases.eval_polynomial(radius_in_km[indx],
                                uniqueregions[region]['radius_range'] ,rnorm,nonspltype)
                        # combine polynomials and splines in the original order
                        vercof = np.zeros([len(indx),len(uniqueregions[region]['types'])]);
                        dvercof = np.zeros([len(indx),len(uniqueregions[region]['types'])]);
                        vercof[:,isspl] = vercof1[:,splindx]; dvercof[:,isspl] = dvercof1[:,splindx]
                        vercof[:,~isspl] = vercof2; dvercof[:,~isspl] = dvercof2
                # build up the coefficient array
                coef = []
                for key in uniqueregions[region]['types']:
                    coef.append(self.metadata['attributes'][parameter][region][key])
                temp = np.dot(vercof,np.array([coef]).T)
                for key, val in enumerate(indx):
                    if temp.ndim == 1:
                        values[val] = temp[key]
                    else:
                        values[val] = temp[key][0]
        # If detailed metadata regarding the basis parameterization is not available
        # interpolated based on the card deck file
        else:
            if self.data is not None:
                if parameter in self.metadata['parameters']:
                    modelval = self.data[parameter].values.quantity.magnitude
                    depth_array = constants.R.to('km').magnitude - self.data['radius'].pint.to('km').values.quantity.magnitude # in km
                    values = np.zeros_like(depth_in_km)
                    # Only select the region to interpolate
                    disc_depth = self.metadata['discontinuities']['delta']['depth'].pint.to('km').values.quantity.magnitude
                    disc_depth.sort()
                    # append a depth to get the index of discontinuity below
                    disc_depth = np.append(disc_depth,constants.R.to('km').magnitude)
                    disc_depth = np.append(0.,disc_depth)
                    for idep,depth in enumerate(depth_in_km):
                        # is it a discontinity
                        if self.if_discontinuity(depth):
                            findindices = np.nonzero(np.isclose(depth_array,depth))[0]
                            if boundary == '+': # larger radius, smaller depth
                                values[idep] = modelval[findindices[1]]
                            elif boundary == '-':
                                values[idep] = modelval[findindices[0]]
                        else: # not a boundary
                            # this is the bottom of the region under consideration
                            indxdisc = np.where(disc_depth-depth>=0.)[0][0]
                            if indxdisc == 0: indxdisc += 1 # surface is queried, so use the region below for interpolation
                            end=np.where(np.isclose(depth_array-disc_depth[indxdisc-1],0))[0][0]
                            if indxdisc != 1:
                                # leave the first and last since these are other regions
                                start=np.where(np.isclose(depth_array-disc_depth[indxdisc],0))[0][1]
                                indxdeps = np.arange(start,end+1)
                            else:
                                start=0
                                indxdeps = np.arange(start,end+1)
                            values[idep] = griddata(depth_array[indxdeps],modelval[indxdeps],depth,method=interpolation)
                else:
                    raise ValueError('parameter '+parameter+' not defined in array')
            else:
                raise ValueError('data in reference1D object is not allocated')
        return values*constants.ureg(unit) 
[docs]    def to_mineoscards(self,directory='.',fmt='cards'):
        '''
        Writes a model file that is compatible with MINEOS.
        '''
        parameters = ['radius','rho','vpv','vsv','qkappa','qmu','vph','vsh','eta']
        units =['m','kg/m^3','m/s','m/s','dimensionless','dimensionless','m/s','m/s','dimensionless']
        if self.data is not None and self._nlayers > 0:
            model_name = self._name
            ntotlev = self._nlayers
            itopic = self.metadata['discontinuities']['itopic']
            itopoc = self.metadata['discontinuities']['itopoc']
            itopmantle = self.metadata['discontinuities']['itopmantle']
            itopcrust = self.metadata['discontinuities']['itopcrust']
            outfile = directory+'/'+model_name+'.'+fmt
            f = open(outfile,'w')
            f.write(model_name+'\n')
            f.write('1 1. 1 1\n')
            line = ff.FortranRecordWriter('(5I5)')
            f.write(line.write([ntotlev,itopic,itopoc,itopmantle,itopcrust])+u'\n')
            line = ff.FortranRecordWriter('(f8.0,3f9.2,2f9.1,2f9.2,f9.5)')
            write = self.data[parameters]
            for i in range(self._nlayers):
                eachline = []
                for indx,param in enumerate(parameters):
                    eachline.append(write.iloc[i][param].to(units[indx]).magnitude)
                f.write(line.write(eachline)+u'\n')
            f.close()
            print('... written mineos file '+outfile)
        else:
            raise ValueError('reference1D object is not allocated') 
[docs]    def to_TauPmodel(self,directory='.',fmt='tvel'):
        '''
        Writes a model file that is compatible with TauP.
        file format options 'tvel' and 'nd'.
        Note: TauP can't handle zero shear velocity in the ocean layer...
          To work around this, zero values an ocean layer will be written
          as 1e-4.
        '''
        if self.data is not None and self._nlayers > 0:
            model_name = self._name
            outfile = directory+'/'+model_name+'.'+fmt
            f = open(outfile,'w')
            f.write('{} - P\n'.format(model_name))
            f.write('{} - S\n'.format(model_name))
            for i in range(self._nlayers):
                d_idx = self._nlayers-i-1
                f.write('{:2.4f}   {:2.4f}   {:2.4f}    {:2.4f}\n'.format(
                   (self._radius_max - self.data['radius'][d_idx]).to('km').magnitude,
                   self.data['vp'][d_idx].to('km/s').magnitude,
                   self.data['vs'][d_idx].to('km/s').magnitude,
                   self.data['rho'][d_idx].to('g/cm^3').magnitude))
            f.close()
            print('... written TauP file '+outfile)
        else:
            raise ValueError('reference1D object is not allocated')
        if self.data['vp'].iloc[-1] == 0 or self.data['vs'].iloc[-1] == 0:
            raise Warning('zero velocity layer detected at surface ...\n \
                      TauP raytracing may not work') 
[docs]    def to_axisem(self,directory='.',anelastic=True,anisotropic=True,fmt='bm'):
        '''
         Write 1D model to be used as an external model in axisem
        '''
        if self.data is not None and self._nlayers > 0:
            model_name = self._name
            outfile = directory+'/'+model_name+'.'+fmt
            f = open(outfile,'w')
            n_discon = 0
            if anelastic:
                f.write('ANELASTIC     T\n')
            else:
                f.write('ANELASTIC     F\n')
            if anisotropic:
                f.write('ANISOTROPIC     T\n')
            else:
                f.write('ANISOTROPIC     F\n')
            f.write('UNITS      m\n')
            if anisotropic:
                f.write('COLUMNS   radius    rho    vpv    vsv    qka    qmu    vph    vsh    eta\n')
            keys = ['radius','rho','vpv','vsv','qkappa','qmu','vph','vsh','eta']
            values =['m','kg/m^3','m/s','m/s','dimensionless','dimensionless','m/s','m/s','dimensionless']
            units = dict(zip(keys, values))
            for i in range(self._nlayers):
                f.write('{}    {}    {}    {}    {}    {}    {}    {}    {}\n'.format(
                self.data['radius'][::-1][i].to(units['radius']).magnitude,
                self.data['rho'][::-1][i].to(units['rho']).magnitude,
                self.data['vpv'][::-1][i].to(units['vpv']).magnitude,
                self.data['vsv'][::-1][i].to(units['vsv']).magnitude,
                self.data['qkappa'][::-1][i].to(units['qkappa']).magnitude,
                self.data['qmu'][::-1][i].to(units['qmu']).magnitude,
                self.data['vph'][::-1][i].to(units['vph']).magnitude,
                self.data['vsh'][::-1][i].to(units['vsh']).magnitude,
                self.data['eta'][::-1][i].to(units['eta']).magnitude) )
                if i < self._nlayers-1 and self.data['radius'][::-1][i] == self.data['radius'][::-1][i+1]:
                    depth_here = (self._radius_max - self.data['radius'][::-1][i]) / 1000.0
                    n_discon += 1
                    f.write('#    Discontinuity {}, depth {:6.2f} km\n'.format(n_discon,depth_here))
            f.close()
            print('... written AXISEM file '+outfile)
        else:
            raise ValueError('reference1D object is not allocated')