Skip to content
Snippets Groups Projects
Forked from Eliu Huerta Escudero / Waveform_Extractor
43 commits behind the upstream repository.
power.py 28.44 KiB
#!/usr/bin/env python

# Copyright (c) 2017 The Board of Trustees of the University of Illinois
# All rights reserved.
#
# Developed by: Daniel Johnson, E. A. Huerta, Roland Haas
#               NCSA Gravity Group
#               National Center for Supercomputing Applications
#               University of Illinois at Urbana-Champaign
#               http://gravity.ncsa.illinois.edu/
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal with 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:
#
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimers.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimers in the documentation
# and/or other materials provided with the distribution.
#
# Neither the names of the National Center for Supercomputing Applications,
# University of Illinois at Urbana-Champaign, nor the names of its
# contributors may be used to endorse or promote products derived from this
# Software without specific prior written permission.
#
# 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
# CONTRIBUTORS 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
# WITH THE SOFTWARE.

# Based off of SimulationTools Mathematica Package
# http://www.simulationtools.org/

import numpy as np
import glob
import os
import h5py
import re
import math
import sys
import warnings
import scipy.optimize
import scipy.interpolate
import scipy.integrate
import argparse
import configparser

#-----Function Definitions-----#

def joinDsets(dsets):
    
        """joints multiple datasets which each have a
        time like first column, eg iteration number of
        time. Removes overlapping segments, keeping the
        last segment.
    
        dsets = iterable of 2d array like objects with data"""
        # joins multiple datasets of which the first column is assumed to be "time"
        if(not dsets):
            return None
        length = 0
        for d in dsets:
            length += len(d)
        newshape = list(dsets[0].shape)
        newshape[0] = length
        dset = np.empty(shape=newshape, dtype=dsets[0].dtype)
        usedlength = 0
        for d in dsets:
            insertpointidx = np.where(dset[0:usedlength,0] >= d[0,0])
            if(insertpointidx[0].size):
                insertpoint = insertpointidx[0][0]
            else:
                insertpoint = usedlength
            newlength = insertpoint+len(d)
            dset[insertpoint:newlength] = d
            usedlength = newlength
        return dset[0:usedlength]


def loadHDF5Series(nameglob, series):
        """load HDF5 timeseries data and concatenate the content of multiple files
    
        nameglob = a shell glob that matches all files to be loaded,
        files are sorted alphabetically
        series = HDF5 dataset name of dataset to load from files"""
        dsets = list()
        for fn in sorted(glob.glob(nameglob)):
            fh = h5py.File(fn, "r")
            dsets.append(fh[series])
        return joinDsets(dsets)



    
#Convert radial to tortoise coordinates
def RadialToTortoise(r, M):
    """
    Convert the radial coordinate to the tortoise coordinate

    r = radial coordinate
    M = ADMMass used to convert coordinate
    return = tortoise coordinate value
    """
    return r + 2. * M * math.log( r / (2. * M) - 1.)

# use fixed frequency integration to integrate psi4 once
def FFIIntegrate(mp_psi4, f0):
    """
    Compute the integral of mmp_psi4 data using fixed frequency integration

    mp_psi4 = Weyl scalar result from simulation
    f0 = cutoff frequency
    return = news of the gravitational wave
    """
    #TODO: Check for uniform spacing in time
    t0 = mp_psi4[:, 0]
    list_len = len(t0)
    complexPsi = mp_psi4[:, 1]

    freq, psif = myFourierTransform(t0, complexPsi)
    hf = ffi(freq, psif, f0)

    time, h = myFourierTransformInverse(freq, hf, t0[0])
    hTable = np.column_stack((time, h))
    return hTable

#Convert modified psi4 to strain
def psi4ToStrain(mp_psi4, f0):
    """
    Convert the input mp_psi4 data to the strain of the gravitational wave
    
    mp_psi4 = Weyl scalar result from simulation
    f0 = cutoff frequency
    return = strain (h) of the gravitational wave
    """
    #TODO: Check for uniform spacing in time
    t0 = mp_psi4[:, 0]
    list_len = len(t0)
    complexPsi = mp_psi4[:, 1]+1.j*mp_psi4[:, 2]

    freq, psif = myFourierTransform(t0, complexPsi)
    dhf = ffi(freq, psif, f0)
    hf = ffi(freq, dhf, f0)

    time, h = myFourierTransformInverse(freq, hf, t0[0])
    hTable = np.column_stack((time, h))
    return hTable

#Fixed frequency integration
# See https://arxiv.org/abs/1508.07250 for method
def ffi(freq, data, f0):
    """
    Integrates the data according to the input frequency and cutoff frequency

    freq = fourier transform frequency
    data = input on which ffi is performed
    f0 = cutoff frequency
    """
    f1 = f0/(2*math.pi)
    fs = freq
    gs = data
    mask1 = (np.sign((fs/f1) - 1) + 1)/2.
    mask2 = (np.sign((-fs/f1) - 1) + 1)/2.
    mask = 1 - (1 - mask1) * (1 - mask2)
    fs2 = mask * fs + (1-mask) * f1 * np.sign(fs - np.finfo(float).eps)
    new_gs = gs/(2*math.pi*1.j*fs2)
    return new_gs

#Fourier Transform
def myFourierTransform(t0, complexPsi):
    """
    Transforms the complexPsi data to frequency space

    t0 = time data points
    complexPsi = data points of Psi to be transformed
    """
    psif = np.fft.fft(complexPsi, norm="ortho")
    l = len(complexPsi)
    n = int(math.floor(l/2.))
    newpsif = psif[l-n:]
    newpsif = np.append(newpsif, psif[:l-n])
    T = np.amin(np.diff(t0))*l
    freq = range(-n, l-n)/T
    return freq, newpsif

#Inverse Fourier Transform
def myFourierTransformInverse(freq, hf, t0):
    l = len(hf)
    n = int(math.floor(l/2.))
    newhf = hf[n:]
    newhf = np.append(newhf, hf[:n])
    amp = np.fft.ifft(newhf, norm="ortho")
    df = np.amin(np.diff(freq))
    time = t0 + range(0, l)/(df*l)
    return time, amp

def angular_momentum(x, q, m, chi1, chi2, LInitNR):
    eta = q/(1.+q)**2
    m1 = (1.+math.sqrt(1.-4.*eta))/2.
    m2 = m - m1
    S1 = m1**2. * chi1
    S2 = m2**2. * chi2
    Sl = S1+S2
    Sigmal = S2/m2 - S1/m1
    DeltaM = m1 - m2
    mu = eta
    nu = eta
    GammaE = 0.5772156649;
    e4 = -(123671./5760.)+(9037.* math.pi**2.)/1536.+(896.*GammaE)/15.+(-(498449./3456.)+(3157.*math.pi**2.)/576.)*nu+(301. * nu**2.)/1728.+(77.*nu**3.)/31104.+(1792. *math.log(2.))/15.
    e5 = -55.13
    j4 = -(5./7.)*e4+64./35.
    j5 = -(2./3.)*e5-4988./945.-656./135. * eta;
    CapitalDelta = (1.-4.*eta)**0.5

    # RH: expression was originally provided by EAH
    # TODO: get referecen for this from EAH
    l = (eta/x**(1./2.)*(
        1. +
        x*(3./2. + 1./6.*eta) + 
        x**2. *(27./8. - 19./8.*eta + 1./24.*eta**2.) + 
        x**3. *(135./16. + (-6889./144. + 41./24. * math.pi**2.)*eta + 31./24.*eta**2. + 7./1296.*eta**3.) + 
        x**4. *((2835./128.) + eta*j4 - (64.*eta*math.log(x)/3.))+ 
        x**5. *((15309./256.) + eta*j5 + ((9976./105.) + (1312.*eta/15.))*eta*math.log(x))+
        x**(3./2.)*(-(35./6.)*Sl - 5./2.*DeltaM* Sigmal) + 
        x**(5./2.)*((-(77./8.) + 427./72.*eta)*Sl + DeltaM* (-(21./8.) + 35./12.*eta)*Sigmal) + 
        x**(7./2.)*((-(405./16.) + 1101./16.*eta - 29./16.*eta**2.)*Sl + DeltaM*(-(81./16.) + 117./4.*eta - 15./16.*eta**2.)*Sigmal) + 
        (1./2. + (m1 - m2)/2. - eta)* chi1**2. * x**2. +
        (1./2. + (m2 - m1)/2. - eta)* chi2**2. * x**2. + 
        2.*eta*chi1*chi2*x**2. +
        ((13.*chi1**2.)/9. +
        (13.*CapitalDelta*chi1**2.)/9. -
        (55.*nu*chi1**2.)/9. - 
        29./9.*CapitalDelta*nu*chi1**2. + 
        (14.*nu**2. *chi1**2.)/9. +
        (7.*nu*chi1*chi2)/3. +
        17./18.* nu**2. * chi1 * chi2 + 
        (13.* chi2**2.)/9. -
        (13.*CapitalDelta*chi2**2.)/9. -
        (55.*nu*chi2**2.)/9. +
        29./9.*CapitalDelta*nu*chi2**2. +
        (14.*nu**2. * chi2**2.)/9.)
        * x**3.))
    return l - LInitNR


def getFinalSpinFromQLM(sim_path):
    mass_path = sorted(glob.glob(os.path.join(sim_path, "output-????", "*", "quasilocalmeasures-qlm_scalars..asc")))
    A_val = np.loadtxt(mass_path[-1])     ## For mass calculation
    M_final = A_val[:,58][-1]
    Sz_final = A_val[:,37][-1]
    a_final = Sz_final / M_final
    return a_final, M_final

def getADMMassFromTwoPunctureBBH(meta_filename):
    """
    Determine cutoff frequency of simulation

    meta_filename = path to TwoPunctures.bbh
    return = initial ADM mass of system
    """

    config = configparser.ConfigParser()
    config.read(meta_filename)

    ADMmass = float(config['metadata']['initial-ADM-energy'])

    return ADMmass

def getCutoffFrequencyFromTwoPuncturesBBH(meta_filename):
    """
    Determine cutoff frequency of simulation

    meta_filename = path to TwoPunctures.bbh
    return = cutoff frequency
    """

    config = configparser.ConfigParser()
    config.read(meta_filename)

    position1x = float(config['metadata']['initial-bh-position1x'])
    position1y = float(config['metadata']['initial-bh-position1y'])
    position1z = float(config['metadata']['initial-bh-position1z'])
    position2x = float(config['metadata']['initial-bh-position2x'])
    position2y = float(config['metadata']['initial-bh-position2y'])
    position2z = float(config['metadata']['initial-bh-position2z'])
    momentum1x = float(config['metadata']['initial-bh-momentum1x'])
    momentum1y = float(config['metadata']['initial-bh-momentum1y'])
    momentum1z = float(config['metadata']['initial-bh-momentum1z'])
    momentum2x = float(config['metadata']['initial-bh-momentum2x'])
    momentum2y = float(config['metadata']['initial-bh-momentum2y'])
    momentum2z = float(config['metadata']['initial-bh-momentum2z'])
    spin1x = float(config['metadata']['initial-bh-spin1x'])
    spin1y = float(config['metadata']['initial-bh-spin1y'])
    spin1z = float(config['metadata']['initial-bh-spin1z'])
    spin2x = float(config['metadata']['initial-bh-spin2x'])
    spin2y = float(config['metadata']['initial-bh-spin2y'])
    spin2z = float(config['metadata']['initial-bh-spin2z'])
    mass1 = float(config['metadata']['initial-bh-puncture-adm-mass1'])
    mass2 = float(config['metadata']['initial-bh-puncture-adm-mass2'])

    angularmomentum1x = position1y * momentum1z - momentum1z * momentum1y
    angularmomentum1y = position1z * momentum1x - momentum1x * momentum1z
    angularmomentum1z = position1x * momentum1y - momentum1y * momentum1x

    angularmomentum2x = position2y * momentum2z - momentum2z * momentum2y
    angularmomentum2y = position2z * momentum2x - momentum2x * momentum2z
    angularmomentum2z = position2x * momentum2y - momentum2y * momentum2x

    angularmomentumx = angularmomentum1x + angularmomentum2x
    angularmomentumy = angularmomentum1y + angularmomentum2y
    angularmomentumz = angularmomentum1z + angularmomentum2z

    LInitNR = math.sqrt(angularmomentumx**2 + angularmomentumy**2 + angularmomentumz**2)
    S1 = math.sqrt(spin1x**2 + spin1y**2 + spin1z**2)
    S2 = math.sqrt(spin2x**2 + spin2y**2 + spin2z**2)

    M = mass1+mass2
    q = mass1/mass2
    chi1 = S1/mass1**2
    chi2 = S2/mass2**2
    # .014 is the initial guess for cutoff frequency
    omOrbPN = scipy.optimize.fsolve(angular_momentum, .014, (q, M, chi1, chi2, LInitNR))[0]
    omOrbPN = omOrbPN**(3./2.)
    omGWPN = 2. * omOrbPN
    omCutoff = 0.75 * omGWPN
    return omCutoff

def getModesInFile(sim_path):
    """
    Find all modes and radii in file.

    sim_path = path to simulation main directory
    return = radii, modes
    """

    fn = sorted(glob.glob(sim_path+"/output-????/*/mp_[Pp]si4.h5"))[0]
    with h5py.File(fn, "r") as fh:
        radii = set()
        modes = set()
        for dset in fh:
            # TODO: extend Multipole to save the radii as attributes and/or
            # use a group structure in the hdf5 file
            m = re.match(r'l(\d*)_m(-?\d*)_r(\d*\.\d)', dset)
            if m:
                radius = float(m.group(3))
                mode = (int(m.group(1)), int(m.group(2)))
                modes.add(mode)
                radii.add(radius)
    modes = sorted(modes)
    radii = sorted(radii)
    return radii, modes



# -----------------------------------------------------------------------------
# POWER Method
# -----------------------------------------------------------------------------
 
    
def POWER(sim_path, radii, modes):
    main_dir = sim_path
    sim = os.path.split(sim_path)[-1]
    
    simdirs = main_dir+"/output-????/%s/" % (sim)
    f0 = getCutoffFrequencyFromTwoPuncturesBBH(main_dir+"/output-0000/%s/TwoPunctures.bbh" % (sim))
    #Get simulation total mass
    ADMMass = getADMMassFromTwoPunctureBBH(main_dir+"/output-0000/%s/TwoPunctures.bbh" % (sim))
    
    # get translation table from (mode, radius) to dataset name
    # TODO: this ought to be handled differently
    dsets = {}
    fn = sorted(glob.glob(sim_path+"/output-????/*/mp_[Pp]si4.h5"))[0]
    with h5py.File(fn, "r") as fh:
        for dset in fh:
            # TODO: extend Multipole to save the radii as attributes and/or
            # use a group structure in the hdf5 file
            m = re.match(r'l(\d*)_m(-?\d*)_r(\d*\.\d)', dset)
            if m:
                radius = float(m.group(3))
                mode = (int(m.group(1)), int(m.group(2)))
                dsets[(radius, mode)] = dset

    #Get Psi4
    extrapolated_strains = []
    for (el,em) in modes:                      # 25 times through the loop, from (1,1) to (4,4)
            mp_psi4_vars = []
            strain = []
            phase = []
            amp = []
            for i in range(len(radii)):      # so 7 times through each mode at each of the 7 radii
                    #------------------------------------------------
                    # Read in HDF5 data
                    #------------------------------------------------
                    radius = radii[i]
                    psi4dsetname = dsets[(radius, (el,em))]
                    mp_psi4 = loadHDF5Series(simdirs+"mp_psi4.h5", psi4dsetname)
                    mp_psi4_vars.append(mp_psi4)
                    

                    #-----------------------------------------
                    # Prepare for conversion to strain
                    #-----------------------------------------
                    # retardate time by estimated travel time to each detector,
                    # convert from psi4 to r*psi4 to account for initial 1/r falloff
                    # RH: it might be even better (though harder to define) to
                    # get a retardating time by looking at the time of the
                    # maximum (found as an optimization over an interpolating
                    # function, not argmax)
                    mp_psi4_vars[i][:, 0] -= RadialToTortoise(radius, ADMMass)
                    mp_psi4_vars[i][:, 1] *= radii[i]
                    mp_psi4_vars[i][:, 2] *= radii[i]
    
                    #Check for psi4 amplitude going to zero
                    # RH: this makes very little sense since the amplitude is
                    # expected to be zero initially and very late
                    psi4_amp = np.sqrt(mp_psi4_vars[i][:, 1]**2 + mp_psi4_vars[i][:, 2]**2)
                    min_psi4_amp = np.amin(psi4_amp)
                    if(min_psi4_amp < np.finfo(float).eps and el >= 2):
                        print("The psi4 amplitude is near zero. The phase is ill-defined.")
    
                    #Fixed-frequency integration twice to get strain
                    #-----------------------------------------------------------------
                    # Strain Conversion
                    #-----------------------------------------------------------------

                    hTable = psi4ToStrain(mp_psi4_vars[i], f0)  # table of strain

                    
                    time = hTable[:, 0]
                    h = hTable[:, 1]
                    hplus = h.real
                    hcross = h.imag
                    newhTable = np.column_stack((time, hplus, hcross))
                    warnings.filterwarnings('ignore')
                    finalhTable = newhTable.astype(float)
                    strain.append(finalhTable)
                    
                    
                    #-------------------------------------------------------------------
                    # Analysis of Strain
                    #-------------------------------------------------------------------
                    #Get phase and amplitude of strain
                    h_phase = np.unwrap(np.angle(h))
                    # print(len(h_phase), "h_phase length")
                    # print(len(time), "time length")
                    angleTable = np.column_stack((time, h_phase))     ### start here
                    angleTable = angleTable.astype(float)             ### b/c t is defined based on
                    phase.append(angleTable)                          ### time here
                    h_amp = np.absolute(h)
                    ampTable = np.column_stack((time, h_amp))
                    ampTable = ampTable.astype(float)
                    amp.append(ampTable)


            #print("h_amp:" , h_amp)

            #----------------------------------------------------------------------
            # Extrapolation
            #----------------------------------------------------------------------

            # get common range in times
            tmin = max([phase[i][ 0,0] for i in range(len(phase))])
            tmax = min([phase[i][-1,0] for i in range(len(phase))])

            # smallest timestep in any series
            dtmin = min([np.amin(np.diff(phase[0][:,0])) for i in range(len(phase))])

            # uniform, common time
            t = np.arange(tmin, tmax, dtmin)

            # Interpolate phase and amplitude
            interpolation_order = 9
            for i in range(len(radii)):
                    interp_function = scipy.interpolate.interp1d(amp[i][:, 0], amp[i][:, 1], kind=interpolation_order)
                    resampled_amp_vals = interp_function(t)
                    amp[i] = np.column_stack((t, resampled_amp_vals))

                    interp_function = scipy.interpolate.interp1d(phase[i][:, 0], phase[i][:, 1], kind=interpolation_order)
                    resampled_phase_vals = interp_function(t)
                    # try and keep all phases at the amplitude maximum within 2pi of each other
                    # alignment is between neighbhours just in case there actually ever is
                    # >2pi difference between the innermost and the ohtermost detector
                    if(i > 0):
                        # for some modes (post 2,2) the initial junk can be the
                        # largest amplitude contribution, so w try to skip it
                        # when looking for maxima
                        junk_time = 50.
                        post_junk_idx_p = amp[i-1][:,0] > junk_time
                        post_junk_idx = amp[i-1][:,0] > junk_time
                        maxargp = np.argmax(amp[i-1][post_junk_idx_p,1])
                        maxarg = np.argmax(amp[i][post_junk_idx,1])
                        phase_shift = round((resampled_phase_vals[post_junk_idx][maxarg] - phase[i-1][post_junk_idx_p][maxargp,1])/(2.*math.pi))*2.*math.pi
                        resampled_phase_vals -= phase_shift
                    phase[i] = np.column_stack((t, resampled_phase_vals))

            #Extrapolate
            phase_extrapolation_order = 1
            amp_extrapolation_order = 2
            radii = np.asarray(radii, dtype=float)
            A_phase = np.ones_like(radii)
            A_amp = np.ones_like(radii)

            for i in range(1, phase_extrapolation_order+1):
                A_phase = np.column_stack((A_phase, np.power(radii, -1*i)))
    
            for i in range(1, amp_extrapolation_order+1):
                A_amp = np.column_stack((A_amp, np.power(radii, -1*i)))
    
            b_phase = np.empty_like(radii, shape=(len(radii), len(t)))
            b_amp = np.empty_like(radii, shape=(len(radii), len(t)))
            for j in range(len(radii)):
                b_phase[j] = phase[j][:, 1]
                b_amp[j] = amp[j][:, 1]

            x_phase = np.linalg.lstsq(A_phase, b_phase)[0]
            radially_extrapolated_phase = x_phase[0]
    
            x_amp = np.linalg.lstsq(A_amp, b_amp)[0]
            radially_extrapolated_amp = x_amp[0]

            radially_extrapolated_h_plus = radially_extrapolated_amp * np.cos(radially_extrapolated_phase)
            radially_extrapolated_h_cross = radially_extrapolated_amp * np.sin(radially_extrapolated_phase)

            extrapolated_strains.append(np.column_stack((t, radially_extrapolated_h_plus, radially_extrapolated_h_cross)))
    return extrapolated_strains
        

# -----------------------------------------------------------------------------
# Nakano Method
# -----------------------------------------------------------------------------

        
def eq_29(sim_path, radii_list, modes):
    
    main_dir = sim_path
    sim = os.path.split(sim_path)[-1]
    simdirs = sim_path+"/output-????/%s/" % (sim)

    # get translation table from (mode, radius) to dataset name
    # TODO: this ought to be handled differently
    dsets = {}
    fn = sorted(glob.glob(sim_path+"/output-????/*/mp_[Pp]si4.h5"))[0]
    with h5py.File(fn, "r") as fh:
        for dset in fh:
            # TODO: extend Multipole to save the radii as attributes and/or
            # use a group structure in the hdf5 file
            m = re.match(r'l(\d*)_m(-?\d*)_r(\d*\.\d)', dset)
            if m:
                radius = float(m.group(3))
                mode = (int(m.group(1)), int(m.group(2)))
                dsets[(radius, mode)] = dset

    a, M = getFinalSpinFromQLM(sim_path)
    f0 = getCutoffFrequencyFromTwoPuncturesBBH(main_dir+"/output-0000/%s/TwoPunctures.bbh" % (sim))

    extrapolated_strains = []
    for (el,em) in modes:
        for radius in radii_list:
        
                ar = loadHDF5Series(simdirs+"mp_psi4.h5" , dsets[(radius, (el,em))])   # loads HDF5 Series from file mp_psi4.h5, specifically the "l%d_m%d_r100.00" ones ... let's loop this over all radii
                
                psi = np.column_stack((ar[:,0], ar[:,1] + 1j * ar[:,2]))
                # 1st column of ar, time data points
                # 2nd column of ar, data points for psi
                # 3rd column of ar, data points for imaginary psi
                
                news = FFIIntegrate(psi, f0)
                strain = FFIIntegrate(news, f0)
            
                # TODO: check if expressions are applicable for el < 2 at all or
                # of Nakano's derivation requires el>=2 to begin with
                A = 1.-(2.*M/radius)
                a_1 = radius
                if el < 1:
                    a_2 = 0.
                    a_3 = 0.
                else:
                    a_2 = (el-1.)*(el+2.)/(2.*radius)
                    # Note: third term is negative for el==1
                    a_3 = (el-1.)*(el+2.)*(el**2 + el - 4.)/(8*radius*radius)

                if el < 1 or (radius, (el+1,em)) not in dsets:
                    psi_a = np.zeros_like(psi)             # ...fill psi_a and impsi_a arrays with zeros (mode is unphysical)
                    dt_psi_a = np.zeros_like(psi)          # ...fill psi_a and impsi_a arrays with zeros (mode is unphysical)
                    B = 0.
                    b_1 = 0.
                    b_2 = 0.
                else:
                    # TODO: throw an error when a physical mode is not present in the file?
                    modes_a = dsets[(radius, (el+1,em))]         # "top" modes
                    ar_a = loadHDF5Series(simdirs+'mp_psi4.h5' , modes_a)
                    psi_a = np.column_stack((ar_a[:,0], ar_a[:,1] + 1j * ar_a[:,2]))
                    dt_psi_a = np.column_stack((psi_a[:,0], np.gradient(psi_a[:,1], psi_a[:,0])))
                    B = 2.j*a/(el+1.)**2*np.sqrt((el+3.)*(el-1)*(el+em+1.)*(el-em+1.)/((2.*el+1.)*(2.*el+3.)))
                    b_1 = 1.
                    b_2 = el*(el+3.)/radius

                if em > el-1 or el < 2:                       # if em is greater than the bottom mode...
                    psi_b = np.zeros_like(psi)             # ...fill psi_b and impsi_b arrays with zeros (mode is unphysical)
                    dt_psi_b = np.zeros_like(psi)          # ...fill psi_b and impsi_b arrays with zeros (mode is unphysical)
                    C = 0.
                    c_1 = 0.
                    c_2 = 0.
                else:
                    modes_b = dsets[(radius, (el-1, em))]    # "bottom" modes
                    ar_b = loadHDF5Series(simdirs+'mp_psi4.h5' , modes_b)
                    psi_b = np.column_stack((ar_b[:,0], ar_b[:,1] + 1j * ar_b[:,2]))
                    dt_psi_b = np.column_stack((psi_b[:,0], np.gradient(psi_b[:,1], psi_b[:,0])))
                    C = 2.j*a/el**2*np.sqrt((el+2.)*(el-2.)*(el+em)*(el-em)/((2.*el-1.)*(2.*el+1.)))
                    c_1 = 1.
                    c_2 = (el-2.)*(el+1.)/radius

                extrapolated_psi_data = A*(a_1*psi[:,1] - a_2*radius*news[:,1] + a_3*radius*strain[:,1]) + B*(b_1*radius*dt_psi_a[:,1] - b_2*radius*psi_a[:,1]) - C*(c_1*radius*dt_psi_b[:,1] - c_2*radius*psi_b[:,1])

                extrapolated_psi = np.column_stack((psi[:,0], extrapolated_psi_data.real, extrapolated_psi_data.imag))

                extrapolated_strain = psi4ToStrain(extrapolated_psi, f0)

                extrapolated_strains.append(np.column_stack(
                  (extrapolated_strain[:,0].real, extrapolated_strain[:,1].real,
                   extrapolated_strain[:,1].imag)))
    return extrapolated_strains


# -----------------------------------------------------------------------------
        
    
### argparse machinery:

def dir_path(string):
    if os.path.isdir(string):
        return string
    else:
        print("Not a directory: %s" %(string))
        # raise NotADirectoryError(string)

parser = argparse.ArgumentParser(description='Choose extrapolation method')
parser.add_argument("method" , choices=["POWER" , "Nakano"] , help="Extrapolation method to be used here")
parser.add_argument('-r', "--radii" , type=int , help="For POWER method; Number of radii to be used", default=7)
parser.add_argument('-m' , "--modes" , type=str , help="For Nakano method; modes to use, l,m. Leave blank to extrapolate over all available modes")
parser.add_argument("path" , type=dir_path , help="Simulation to be used here")
args = parser.parse_args()

if args.method == "POWER":        
    print("Extrapolating with POWER method...")
    all_radii, all_modes = getModesInFile(args.path)
    radii = all_radii[0:args.radii]
    modes = all_modes

    strains = POWER(args.path, radii, modes)

    #Create data directories
    sim = os.path.split(args.path)[-1]
    main_directory = "Extrapolated_Strain"
    sim_dir = main_directory + "/" + sim
    if not os.path.exists(main_directory):
            os.makedirs(main_directory)
    if not os.path.exists(sim_dir):
            os.makedirs(sim_dir)

    for i, (el,em) in enumerate(modes):
        np.savetxt("./Extrapolated_Strain/"+sim+"/"+sim+"_radially_extrapolated_strain_l"+str(el)+"_m"+str(em)+".dat", strains[i])

    
    

elif args.method == "Nakano":
    print("Extrapolating with Nakano method...")
    all_radii, all_modes = getModesInFile(args.path)
    radii = all_radii[0:args.radii]
    modes = [(0, 0), (1, 0), (1, 1), (2, -1), (2, 0), (2, 1), (2, 2), (3, -2), (3, -1), (3, 0), (3, 1), (3, 2), (3, 3)]

    strains = eq_29(args.path, radii, modes)

    #Create data directories
    sim = os.path.split(args.path)[-1]
    main_directory = "Extrapolated_Strain(Nakano_Kerr)"
    sim_dir = main_directory + "/" + sim
    if not os.path.exists(main_directory):
            os.makedirs(main_directory)
    if not os.path.exists(sim_dir):
            os.makedirs(sim_dir)

    strain = iter(strains)
    for (el,em) in modes:
        for r in radii:
            fn = "%s_f2_l%d_m%d_r%.2f_Looped.dat" % (sim, el, em, r)
            np.savetxt("./Extrapolated_Strain(Nakano_Kerr)/"+sim+"/"+fn, next(strain))