# %%
# Initialization, class definition, and declaration.

import os
import math
from obspy import UTCDateTime
import numpy as np
import copy

class Event():      # class of earthquake
    def __init__(self):
        self.name = "nan"       # evname1 Earthquake name, recommended as "earthquake".
        self.id = -1
        self.lat = 0.0
        self.lon = 0.0
        self.dep = 0.0
        self.mag = 0.0
        self.ortime = UTCDateTime(1999,1,1,0,0,0)
        self.Nt = 0             # Number of the absolute traveltime of earthquake
        self.Ncs_dt = 0         # Number of the commmon source differential traveltime of earthquake
        self.Ncr_dt = 0         # Number of the commmon receiver differential traveltime of earthquake
        self.t = {}             # stname1+phase -> (stname1, phase, time, data_weight)
        self.cs_dt = {}         # stname1 + stname2 + phase -> (stname1, stname2, phase, dif_time, data_weight)
        self.cr_dt = {}         # stname1 + evname2 + phase -> (stname1, evname2, phase, dif_time, data_weight)
        self.azi_gap = 360.0    # the max azimuthal gap of each earthquake
        self.misfit = {}        # traveltime residual of the data, the difference between real data and synthetic data, used for evaluation. stname or stname1+stname2 or stname1+evname2 -> residual
        self.tag    = {}        # additional tags for the earthquake, e.g., azi_gap, weight. (azimuthal gap, weight of the earthquake)

class Station():
    def __init__(self):
        self.name = "nan"       # stname1, recommend: network.stname
        self.id = -1
        self.lat = 0.0
        self.lon = 0.0
        self.ele = 0.0
        self.tag = {}           # additional tags for the station, e.g., wright




# %% [markdown]
# Functions: some basic auxiliary functions for processing data

# %%
# function： cal_dis(lat1, lon1,lat2, lon2) (in kilometers)， cal_azimuth(lat1, lon1, lat2, lon2) (degree) calculate epicentral distance (km) and azimuth (degree)

def cal_dis(lat1, lon1,lat2, lon2, R = 6371):
    latitude1 = (math.pi/180)*lat1
    latitude2 = (math.pi/180)*lat2
    longitude1 = (math.pi/180)*lon1
    longitude2= (math.pi/180)*lon2
    # Therefore, the spherical distance between points A and B is:{arccos[sinb*siny+cosb*cosy*cos(a-x)]}*R
    # Radius of the earth
    if((lat1-lat2)**2+(lon1-lon2)**2<0.000001):
        return 0

    d = math.acos(math.sin(latitude1)*math.sin(latitude2)+ math.cos(latitude1)*math.cos(latitude2)*math.cos(longitude2-longitude1))/math.pi*180
    return d * 2 * math.pi * R / 360

def cal_azimuth(lat1, lon1, lat2, lon2):
    lat1_rad = lat1 * math.pi / 180
    lon1_rad = lon1 * math.pi / 180
    lat2_rad = lat2 * math.pi / 180
    lon2_rad = lon2 * math.pi / 180

    y = math.sin(lon2_rad - lon1_rad) * math.cos(lat2_rad)
    x = math.cos(lat1_rad) * math.sin(lat2_rad) - math.sin(lat1_rad) * math.cos(lat2_rad) * math.cos(lon2_rad - lon1_rad)
    brng = math.atan2(y, x) * 180 / math.pi
    if((lat1-lat2)**2+(lon1-lon2)**2<0.0001):
        return 0
    return float((brng + 360.0) % 360.0)


# %%
# Function: Coordinate rotation rotate_src_rec(ev_info, st_info, theta0, phi0, psi): rotate to the new coordinate system, satisfying the center point transformation r0, t0, p0 -> r0, 0, 0 and an anticlockwise rotation angle psi.
# Satisfying the center point transformation r0, t0, p0 -> r0, 0, 0 and an anticlockwise rotation angle psi.

import numpy as np

RAD2DEG = 180/np.pi
DEG2RAD = np.pi/180
R_earth = 6371.0

# Spherical coordinates to Cartesian coordinate
def rtp2xyz(r,theta,phi):
    x = r * np.cos(theta*DEG2RAD) * np.cos(phi*DEG2RAD)
    y = r * np.cos(theta*DEG2RAD) * np.sin(phi*DEG2RAD)
    z = r * np.sin(theta*DEG2RAD)
    return (x,y,z)

# Cartesian coordinates to Spherical coordinate
def xyz2rtp(x,y,z):
    # theta: -90~90;  phi: -180~180
    r       = np.sqrt(x**2+y**2+z**2)
    theta   = np.arcsin(z/r)
    phi     = np.arcsin(y/r/np.cos(theta))


    idx = np.where((phi > 0) & (x*y < 0))
    phi[idx] = np.pi - phi[idx]
    idx = np.where((phi < 0) & (x*y > 0))
    phi[idx] = -np.pi - phi[idx]


    # for i in range(phi.size):
    #     if(phi[i] > 0 and x[i]*y[i] < 0):
    #         phi[i] = np.pi - phi[i]
    #     if(phi[i] < 0 and x[i]*y[i] > 0):
    #         phi[i] = -np.pi - phi[i]

    return (r,theta*RAD2DEG,phi*RAD2DEG)

# anti-clockwise rotation along x-axis
def rotate_x(x,y,z,theta):
    new_x = x
    new_y = y *  np.cos(theta*DEG2RAD) + z * -np.sin(theta*DEG2RAD)
    new_z = y *  np.sin(theta*DEG2RAD) + z *  np.cos(theta*DEG2RAD)
    return (new_x,new_y,new_z)

# anti-clockwise rotation along y-axis
def rotate_y(x,y,z,theta):
    new_x = x *  np.cos(theta*DEG2RAD) + z *  np.sin(theta*DEG2RAD)
    new_y = y
    new_z = x * -np.sin(theta*DEG2RAD) + z *  np.cos(theta*DEG2RAD)
    return (new_x,new_y,new_z)

# anti-clockwise rotation along z-axis
def rotate_z(x,y,z,theta):
    new_x = x *  np.cos(theta*DEG2RAD) + y * -np.sin(theta*DEG2RAD)
    new_y = x *  np.sin(theta*DEG2RAD) + y *  np.cos(theta*DEG2RAD)
    new_z = z
    return (new_x,new_y,new_z)

# spherical Rotation

# rotate to the new coordinate, satisfying the center r0,t0,p0 -> r0,0,0 and a anticlockwise angle psi
def rtp_rotation(t,p,theta0,phi0,psi):
    # step 1: r,t,p -> x,y,z
    (x,y,z) = rtp2xyz(1.0,t,p)

    # step 2: anti-clockwise rotation with -phi0 along z-axis:   r0,t0,p0 -> r0,t0,0
    (x,y,z) = rotate_z(x,y,z,-phi0)

    # step 3: anti-clockwise rotation with theta0 along y-axis:  r0,t0,0 -> r0,0,0
    (x,y,z) = rotate_y(x,y,z,theta0)

    # # step 4: anti-clockwise rotation with psi along x-axis
    (x,y,z) = rotate_x(x,y,z,psi)

    # step 5: x,y,z -> r,t,p
    (new_r,new_t,new_p) = xyz2rtp(x,y,z)

    return (new_t,new_p)


def rtp_rotation_reverse(new_t,new_p,theta0,phi0,psi):
    # step 1: r,t,p -> x,y,z
    (x,y,z) = rtp2xyz(1.0,new_t,new_p)

    # step 2: anti-clockwise rotation with -psi along x-axis
    (x,y,z) = rotate_x(x,y,z,-psi)

    # step 3: anti-clockwise rotation with -theta0 along y-axis:  r0,0,0 -> r0,t0,0
    (x,y,z) = rotate_y(x,y,z,-theta0)

    # step 4: anti-clockwise rotation with phi0 along z-axis:   r0,t0,0 -> r0,t0,p0
    (x,y,z) = rotate_z(x,y,z,phi0)

    # step 5: x,y,z -> r,t,p
    (r,t,p) = xyz2rtp(x,y,z)

    return (t,p)

def rotate_src_rec(ev_info,st_info,theta0,phi0,psi):
    ev_info_rotate = {}
    st_info_rotate = {}

    # rotate earthquakes
    for key_ev in ev_info:
        ev = ev_info[key_ev]
        ev_lat = np.array([ev.lat]); ev_lon = np.array([ev.lon])
        (ev_lat,ev_lon,) = rtp_rotation(ev_lat,ev_lon,theta0,phi0,psi)
        ev.lat = ev_lat[0]; ev.lon = ev_lon[0]
        ev_info_rotate[key_ev] = ev

    # rotate stations
    for key_st in st_info:
        st = st_info[key_st]
        st_lat = np.array([st.lat]); st_lon = np.array([st.lon])
        (st_lat,st_lon) = rtp_rotation(st_lat,st_lon,theta0,phi0,psi)
        st.lat = st_lat[0]; st.lon = st_lon[0]
        st_info_rotate[key_st] = st

    return (ev_info_rotate,st_info_rotate)

def rotate_src_rec_reverse(ev_info_rotate,st_info_rotate,theta0,phi0,psi):
    ev_info = {}
    st_info = {}

    # rotate earthquakes
    for key_ev in ev_info_rotate:
        ev = ev_info_rotate[key_ev]
        ev_lat = np.array([ev.lat]); ev_lon = np.array([ev.lon])
        (ev_lat,ev_lon,) = rtp_rotation_reverse(ev_lat,ev_lon,theta0,phi0,psi)
        ev.lat = ev_lat[0]; ev.lon = ev_lon[0]
        ev_info[key_ev] = ev

    # rotate stations
    for key_st in st_info_rotate:
        st = st_info_rotate[key_st]
        st_lat = np.array([st.lat]); st_lon = np.array([st.lon])
        (st_lat,st_lon) = rtp_rotation_reverse(st_lat,st_lon,theta0,phi0,psi)
        st.lat = st_lat[0]; st.lon = st_lon[0]
        st_info[key_st] = st

    return (ev_info,st_info)

# %%
# # Function: Coordinate rotation rotate_src_rec(ev_info, st_info, theta0, phi0, psi): rotate to the new coordinate system, satisfying the center point transformation r0, t0, p0 -> r0, 0, 0 and an anticlockwise rotation angle psi.
# # Satisfying the center point transformation r0, t0, p0 -> r0, 0, 0 and an anticlockwise rotation angle psi.

# import numpy as np

# RAD2DEG = 180/np.pi
# DEG2RAD = np.pi/180
# R_earth = 6371.0

# # Spherical coordinates to Cartesian coordinate
# def rtp2xyz(r,theta,phi):
#     x = r * np.cos(theta*DEG2RAD) * np.cos(phi*DEG2RAD)
#     y = r * np.cos(theta*DEG2RAD) * np.sin(phi*DEG2RAD)
#     z = r * np.sin(theta*DEG2RAD)
#     return (x,y,z)

# # Cartesian coordinates to Spherical coordinate
# def xyz2rtp(x,y,z):
#     # theta: -90~90;  phi: -180~180
#     r       = np.sqrt(x**2+y**2+z**2)
#     theta   = np.arcsin(z/r)
#     phi     = np.arcsin(y/r/np.cos(theta))


#     if(phi > 0 and x*y < 0):
#         phi = np.pi - phi
#     if(phi < 0 and x*y > 0):
#         phi = -np.pi - phi

#     return (r,theta*RAD2DEG,phi*RAD2DEG)

# # anti-clockwise rotation along x-axis
# def rotate_x(x,y,z,theta):
#     new_x = x
#     new_y = y *  np.cos(theta*DEG2RAD) + z * -np.sin(theta*DEG2RAD)
#     new_z = y *  np.sin(theta*DEG2RAD) + z *  np.cos(theta*DEG2RAD)
#     return (new_x,new_y,new_z)

# # anti-clockwise rotation along y-axis
# def rotate_y(x,y,z,theta):
#     new_x = x *  np.cos(theta*DEG2RAD) + z *  np.sin(theta*DEG2RAD)
#     new_y = y
#     new_z = x * -np.sin(theta*DEG2RAD) + z *  np.cos(theta*DEG2RAD)
#     return (new_x,new_y,new_z)

# # anti-clockwise rotation along z-axis
# def rotate_z(x,y,z,theta):
#     new_x = x *  np.cos(theta*DEG2RAD) + y * -np.sin(theta*DEG2RAD)
#     new_y = x *  np.sin(theta*DEG2RAD) + y *  np.cos(theta*DEG2RAD)
#     new_z = z
#     return (new_x,new_y,new_z)

# # spherical Rotation

# # rotate to the new coordinate, satisfying the center r0,t0,p0 -> r0,0,0 and a anticlockwise angle psi
# def rtp_rotation(t,p,theta0,phi0,psi):
#     # step 1: r,t,p -> x,y,z
#     (x,y,z) = rtp2xyz(1.0,t,p)

#     # step 2: anti-clockwise rotation with -phi0 along z-axis:   r0,t0,p0 -> r0,t0,0
#     (x,y,z) = rotate_z(x,y,z,-phi0)

#     # step 3: anti-clockwise rotation with theta0 along y-axis:  r0,t0,0 -> r0,0,0
#     (x,y,z) = rotate_y(x,y,z,theta0)

#     # # step 4: anti-clockwise rotation with psi along x-axis
#     (x,y,z) = rotate_x(x,y,z,psi)

#     # step 5: x,y,z -> r,t,p
#     (new_r,new_t,new_p) = xyz2rtp(x,y,z)

#     return (new_t,new_p)


# def rtp_rotation_reverse(new_t,new_p,theta0,phi0,psi):
#     # step 1: r,t,p -> x,y,z
#     (x,y,z) = rtp2xyz(1.0,new_t,new_p)

#     # step 2: anti-clockwise rotation with -psi along x-axis
#     (x,y,z) = rotate_x(x,y,z,-psi)

#     # step 3: anti-clockwise rotation with -theta0 along y-axis:  r0,0,0 -> r0,t0,0
#     (x,y,z) = rotate_y(x,y,z,-theta0)

#     # step 4: anti-clockwise rotation with phi0 along z-axis:   r0,t0,0 -> r0,t0,p0
#     (x,y,z) = rotate_z(x,y,z,phi0)

#     # step 5: x,y,z -> r,t,p
#     (r,t,p) = xyz2rtp(x,y,z)

#     return (t,p)

# def rotate_src_rec(ev_info,st_info,theta0,phi0,psi):
#     ev_info_rotate = {}
#     st_info_rotate = {}

#     # rotate earthquakes
#     for key_ev in ev_info:
#         ev = ev_info[key_ev]
#         (ev.lat,ev.lon,) = rtp_rotation(ev.lat,ev.lon,theta0,phi0,psi)
#         ev_info_rotate[key_ev] = ev

#     # rotate stations
#     for key_st in st_info:
#         st = st_info[key_st]
#         (st.lat,st.lon) = rtp_rotation(st.lat,st.lon,theta0,phi0,psi)
#         st_info_rotate[key_st] = st

#     return (ev_info_rotate,st_info_rotate)

# def rotate_src_rec_reverse(ev_info_rotate,st_info_rotate,theta0,phi0,psi):
#     ev_info = {}
#     st_info = {}

#     # rotate earthquakes
#     for key_ev in ev_info_rotate:
#         ev = ev_info_rotate[key_ev]
#         (ev.lat,ev.lon) = rtp_rotation_reverse(ev.lat,ev.lon,theta0,phi0,psi)
#         ev_info[key_ev] = ev

#     # rotate stations
#     for key_st in st_info_rotate:
#         st = st_info_rotate[key_st]
#         (st.lat,st.lon) = rtp_rotation_reverse(st.lat,st.lon,theta0,phi0,psi)
#         st_info[key_st] = st

#     return (ev_info,st_info)

# %%
# linear_regression(X,Y)
def linear_regression(X,Y):
    slope,intercept = np.polyfit(X,Y,deg=1)
    fitted_values = slope * X + intercept
    residual = Y - fitted_values
    SEE = np.std(residual)
    return (slope,intercept,SEE)

# %% [markdown]
# 
# Functions: obtain target information from ev_info and st_info

# %%
# function: output the [lon,lat,dep,weight] of the earthquake
def data_lon_lat_dep_wt_ev(ev_info):
    lat = []
    lon = []
    dep = []
    weight = []
    for key in ev_info:
        lat.append(ev_info[key].lat)
        lon.append(ev_info[key].lon)
        dep.append(ev_info[key].dep)
        try:
            weight.append(ev_info[key].tag["weight"])
        except:
            weight.append(1.0)
    return [np.array(lon),np.array(lat),np.array(dep),np.array(weight)]

# %%
# function: output the [lon, lat, dep, ortime] of the earthquake
def data_ev_loc(ev_info):
    lat = []
    lon = []
    dep = []
    ortime = []
    for key in ev_info:
        lat.append(ev_info[key].lat)
        lon.append(ev_info[key].lon)
        dep.append(ev_info[key].dep)
        ortime.append(ev_info[key].ortime.timestamp)
    return [np.array(lon),np.array(lat),np.array(dep),np.array(ortime)]

# %%
# function: output the [lon,lat,dep,weight] of the station
def data_lon_lat_ele_wt_st(ev_info,st_info):
    names = {}
    lat = []
    lon = []
    ele = []
    weight  = []
    for key_ev in ev_info:
        for key_t in ev_info[key_ev].t:     # absolute traveltime data
            name_st = ev_info[key_ev].t[key_t][0]
            names[name_st] = name_st

        for key_t in ev_info[key_ev].cs_dt: # common source differential traveltime data
            name_st = ev_info[key_ev].cs_dt[key_t][0]
            names[name_st] = name_st
            name_st = ev_info[key_ev].cs_dt[key_t][1]
            names[name_st] = name_st

        for key_t in ev_info[key_ev].cr_dt: # common receiver differential traveltime data
            name_st = ev_info[key_ev].cr_dt[key_t][0]
            names[name_st] = name_st

    for name in names:  # only output the station which has data
        lat.append(st_info[name].lat)
        lon.append(st_info[name].lon)
        ele.append(st_info[name].ele)
        try:
            weight.append(st_info[name].tag["weight"])
        except:
            weight.append(1.0)
    return [np.array(lon),np.array(lat),np.array(ele),np.array(weight)]

# %%
# function: output the [dis,time] of all data
def data_dis_time(ev_info,st_info):
    all_dis = []
    all_time = []
    for key_ev in ev_info:
        lat_ev = ev_info[key_ev].lat
        lon_ev = ev_info[key_ev].lon
        dep_ev = ev_info[key_ev].dep
        for key_t in ev_info[key_ev].t:
            all_time.append(ev_info[key_ev].t[key_t][2])
            lat_st = st_info[ev_info[key_ev].t[key_t][0]].lat
            lon_st = st_info[ev_info[key_ev].t[key_t][0]].lon
            ele_st = st_info[ev_info[key_ev].t[key_t][0]].ele
            dis = math.sqrt(cal_dis(lat_ev,lon_ev,lat_st,lon_st)**2 + (dep_ev+ele_st/1000)**2)
            all_dis.append(dis)

    return [np.array(all_dis),np.array(all_time)]

# %%
# function: output the [epidis,time] of all data
def data_epidis_time(ev_info,st_info):
    all_dis = []
    all_time = []
    for key_ev in ev_info:
        lat_ev = ev_info[key_ev].lat
        lon_ev = ev_info[key_ev].lon
        for key_t in ev_info[key_ev].t:
            all_time.append(ev_info[key_ev].t[key_t][2])
            lat_st = st_info[ev_info[key_ev].t[key_t][0]].lat
            lon_st = st_info[ev_info[key_ev].t[key_t][0]].lon
            dis = cal_dis(lat_ev,lon_ev,lat_st,lon_st)**2
            all_dis.append(dis)

    return [np.array(all_dis),np.array(all_time)]

# %%
# function: output the [cs_dt] of all data
def data_cs_dt(ev_info):
    all_time = []
    for key_ev in ev_info:
        for key_dt in ev_info[key_ev].cs_dt:
            all_time.append(ev_info[key_ev].cs_dt[key_dt][3])

    return np.array(all_time)

# %%
# Function: data_dis_time_phase(ev_info, st_info, phase_list) Given a list of seismic phases, output the [epicentral distance, arrival time] for each phase.
def data_dis_time_phase(ev_info,st_info,phase_list):
    all_dis  = {}
    all_time = {}
    for phase in phase_list:
        all_dis[phase] = []
        all_time[phase] = []

    for key_ev in ev_info:
        lat_ev = ev_info[key_ev].lat
        lon_ev = ev_info[key_ev].lon
        dep_ev = ev_info[key_ev].dep
        for key_t in ev_info[key_ev].t:
            phase = key_t.split("+")[1]
            if (not phase in phase_list):
                continue

            all_time[phase].append(ev_info[key_ev].t[key_t][2])
            lat_st = st_info[ev_info[key_ev].t[key_t][0]].lat
            lon_st = st_info[ev_info[key_ev].t[key_t][0]].lon
            ele_st = st_info[ev_info[key_ev].t[key_t][0]].ele

            dis = math.sqrt(cal_dis(lat_ev,lon_ev,lat_st,lon_st)**2 + (dep_ev+ele_st/1000)**2)
            all_dis[phase].append(dis)

    for phase in phase_list:
        all_dis[phase] = np.array(all_dis[phase])
        all_time[phase] = np.array(all_time[phase])

    return [all_dis,all_time]

# %%
# Function: data_lon_lat_dep_wt_ev(ev_info) Outputs the lines connecting station and earthquake for traveltime data as [line_x, line_y].

def data_line(ev_info,st_info):
    line_x = []
    line_y = []

    for key_ev in ev_info:
        lat_ev = ev_info[key_ev].lat
        lon_ev = ev_info[key_ev].lon
        for key_t in ev_info[key_ev].t:
            lat_st = st_info[ev_info[key_ev].t[key_t][0]].lat
            lon_st = st_info[ev_info[key_ev].t[key_t][0]].lon

            line_x.append([lon_ev,lon_st])
            line_y.append([lat_ev,lat_st])

    return [line_x,line_y]

# %% [markdown]
# Functions: discard some data in ev_info and st_info based on selection criteria

# %%
# Function: limit_ev_region(ev_info, lat1, lat2, lon1, lon2, dep1, dep2) Delete the earthquakes that are out of the specified region.

def limit_ev_region(ev_info,lat_min,lat_max,lon_min,lon_max,dep_min,dep_max):
    count_delete = 0

    del_key_ev = []
    for key_ev in ev_info:
        ev = ev_info[key_ev]
        lat = ev.lat
        lon = ev.lon
        dep = ev.dep

        if (lat < min(lat_min,lat_max) or lat > max(lat_min,lat_max)    \
            or lon < min(lon_min,lon_max) or lon > max(lon_min,lon_max)  \
            or dep < min(dep_min,dep_max) or dep > max(dep_min,dep_max)):
            del_key_ev.append(key_ev)
            count_delete += 1

        del_key_t = []
        for key_t in ev_info[key_ev].cr_dt:
            name_ev2 = ev_info[key_ev].cr_dt[key_t][1]
            lat2 = ev_info[name_ev2].lat
            lon2 = ev_info[name_ev2].lon
            dep2 = ev_info[name_ev2].dep
            if (lat2 < min(lat_min,lat_max) or lat2 > max(lat_min,lat_max)    \
                or lon2 < min(lon_min,lon_max) or lon2 > max(lon_min,lon_max) \
                or dep2 < min(dep_min,dep_max) or dep2 > max(dep_min,dep_max)):

                del_key_t.append(key_t)

        for key_t in del_key_t:
            del ev_info[key_ev].cr_dt[key_t]

        ev_info[key_ev].Ncr_dt = len(ev_info[key_ev].cr_dt)

    for key_ev in del_key_ev:
        del ev_info[key_ev]

    print("delete %d events out of the region, now %d earthquakes are retained within the study region"%(count_delete,len(ev_info)))
    return ev_info

# %%
# Function: limit_st_region(ev_info, st_info, lat1, lat2, lon1, lon2) Delete the stations that are out of the specified region.

def limit_st_region(ev_info,st_info,lat1,lat2,lon1,lon2):

    for key_ev in ev_info:
        # delete the station out of the region in the absolute traveltime data
        del_key_t = []
        for key_t in ev_info[key_ev].t:
            name_st = ev_info[key_ev].t[key_t][0]
            lat_st = st_info[name_st].lat
            lon_st = st_info[name_st].lon
            if(lat_st < min(lat1,lat2) or lat_st > max(lat1,lat2) or lon_st < min(lon1,lon2) or lon_st > max(lon1,lon2)):
                del_key_t.append(key_t)

        for key_t in del_key_t:
            del ev_info[key_ev].t[key_t]
        ev_info[key_ev].Nt = len(ev_info[key_ev].t)

        # delete the station out of the region in the common source differential traveltime data
        del_key_t = []
        for key_t in ev_info[key_ev].cs_dt:
            name_st1 = ev_info[key_ev].cs_dt[key_t][0]
            lat_st1 = st_info[name_st1].lat
            lon_st1 = st_info[name_st1].lon

            name_st2 = ev_info[key_ev].cs_dt[key_t][1]
            lat_st2 = st_info[name_st2].lat
            lon_st2 = st_info[name_st2].lon
            if(lat_st1 < min(lat1,lat2) or lat_st1 > max(lat1,lat2) or lon_st1 < min(lon1,lon2) or lon_st1 > max(lon1,lon2) \
                or lat_st2 < min(lat1,lat2) or lat_st2 > max(lat1,lat2) or lon_st2 < min(lon1,lon2) or lon_st2 > max(lon1,lon2)):
                del_key_t.append(key_t)

        for key_t in del_key_t:
            del ev_info[key_ev].cs_dt[key_t]
        ev_info[key_ev].Ncs_dt = len(ev_info[key_ev].cs_dt)

        # delete the station out of the region in the common receiver differential traveltime data
        del_key_st = []
        for key_t in ev_info[key_ev].cr_dt:
            name_st = ev_info[key_ev].cr_dt[key_t][0]
            lat_st = st_info[name_st].lat
            lon_st = st_info[name_st].lon
            if(lat_st < min(lat1,lat2) or lat_st > max(lat1,lat2) or lon_st < min(lon1,lon2) or lon_st > max(lon1,lon2)):
                del_key_st.append(key_t)

        for key_t in del_key_st:
            del ev_info[key_ev].cr_dt[key_t]
        ev_info[key_ev].Ncr_dt = len(ev_info[key_ev].cr_dt)

    return ev_info



# %%
# Function: limit_epi_dis(ev_info, st_info, epi_dis1, epi_dis2) Delete the stations with epicentral distance in the range from epi_dis1 to epi_dis2.

def limit_epi_dis(ev_info,st_info,epi_dis1,epi_dis2):

    for key_ev in ev_info:
        ev = ev_info[key_ev]

        lat_ev = ev.lat
        lon_ev = ev.lon

        # delete the absolute traveltime data
        del_key_t = []
        for key_t in ev.t:
            stname = ev.t[key_t][0]
            lat_st = st_info[stname].lat
            lon_st = st_info[stname].lon
            dis = cal_dis(lat_ev, lon_ev, lat_st, lon_st)
            if (dis > epi_dis1 and dis < epi_dis2):
                del_key_t.append(key_t)
        for key_t in del_key_t:
            del ev.t[key_t]
        ev.Nt = len(ev.t)

        # delete the common source differential traveltime data
        del_key_t = []
        for key_t in ev.cs_dt:
            for i in range(2):
                stname = ev.t[key_t][i]
                lat_st = st_info[stname].lat
                lon_st = st_info[stname].lon
                dis = cal_dis(lat_ev, lon_ev, lat_st, lon_st)

                if (dis > epi_dis1 and dis < epi_dis2):
                    del_key_t.append(key_t)
                    break
        for key_t in del_key_t:
            del ev.cs_dt[key_t]
        ev.Ncs_dt = len(ev.cs_dt)

        # delete the common receiver differential traveltime data
        del_key_t = []
        for key_t in ev.cr_dt:
            stname = ev.cr_dt[key_t][0]
            lat_st = st_info[stname].lat
            lon_st = st_info[stname].lon
            dis = cal_dis(lat_ev, lon_ev, lat_st, lon_st)
            if (dis > epi_dis1 and dis < epi_dis2):
                del_key_t.append(key_t)

            lat_ev2 = ev_info[ev.cr_dt[key_t][1]].lat
            lon_ev2 = ev_info[ev.cr_dt[key_t][1]].lon
            dis = cal_dis(lat_ev2, lon_ev2, lat_st, lon_st)
            if (dis > epi_dis1 and dis < epi_dis2):
                del_key_t.append(key_t)

        for key_t in del_key_t:
            del ev.cr_dt[key_t]
        ev.Ncr_dt = len(ev.cr_dt)


        ev_info[key_ev] = ev

    return ev_info

# %%
# Function: limit_data_residual(ev_info, st_info, slope, intercept, up, down) Limit the data within the range defined by the line time = dis * slope + intercept and the bounds up and down.

# remove outliers, only retain data satisfying:     slope * dis + intercept + down < time < slope * dis + intercept + up
def limit_data_residual(ev_info,st_info,slope,intercept,up,down):
    for key_ev in ev_info:
        lat_ev = ev_info[key_ev].lat
        lon_ev = ev_info[key_ev].lon
        dep_ev = ev_info[key_ev].dep
        del_key_t = []
        for key_t in ev_info[key_ev].t:
            name_st = ev_info[key_ev].t[key_t][0]
            lat_st = st_info[name_st].lat
            lon_st = st_info[name_st].lon
            ele_st = st_info[name_st].ele
            dis = math.sqrt(cal_dis(lat_ev,lon_ev,lat_st,lon_st)**2 + (dep_ev+ele_st/1000)**2)
            residual = ev_info[key_ev].t[key_t][2] - (slope*dis+intercept)

            if (residual < down or residual > up):
                del_key_t.append(key_t)

        for key_t in del_key_t:
            del ev_info[key_ev].t[key_t]

    for key_ev in ev_info:
        ev_info[key_ev].Nt = len(ev_info[key_ev].t)

    return ev_info



# %%
# Function: limit_data_phase(ev_info, phase_list) Retain only the specified seismic phases.

def limit_data_phase(ev_info,phase_list):
    for key_ev in ev_info:
        # process the absolute traveltime data
        new_t = {}
        for key_t in ev_info[key_ev].t:
            phase = ev_info[key_ev].t[key_t][1]
            if phase in phase_list:
                new_t[key_t] = ev_info[key_ev].t[key_t]

        ev_info[key_ev].t = new_t
        ev_info[key_ev].Nt = len(ev_info[key_ev].t)

        # process the common source differential traveltime data
        new_t = {}
        for key_t in ev_info[key_ev].cs_dt:
            phase = ev_info[key_ev].cs_dt[key_t][2]
            phase = phase.split(",")[0]
            if phase in phase_list:
                new_t[key_t] = ev_info[key_ev].cs_dt[key_t]

        ev_info[key_ev].cs_dt = new_t
        ev_info[key_ev].Ncs_dt = len(ev_info[key_ev].cs_dt)

        # process the common receiver differential traveltime data
        new_t = {}
        for key_t in ev_info[key_ev].cr_dt:
            phase = ev_info[key_ev].cr_dt[key_t][2]
            phase = phase.split(",")[0]
            if phase in phase_list:
                new_t[key_t] = ev_info[key_ev].cr_dt[key_t]

        ev_info[key_ev].cr_dt = new_t
        ev_info[key_ev].Ncr_dt = len(ev_info[key_ev].cr_dt)

    return ev_info

# %%
# Function: limit_min_Nt(min_Nt_thd, ev_info) Delete the earthquakes with the number of data less than min_Nt_thd.

def limit_min_Nt(min_Nt_thd, ev_info):
    Nev = len(ev_info)

    del_key_ev = []
    for key_ev in ev_info:
        if(ev_info[key_ev].Nt < min_Nt_thd):
            del_key_ev.append(key_ev)

    for key_ev in del_key_ev:
        del ev_info[key_ev]

    print("Original data set has %d earthquakes, %d earthquakes are deleted, %d earthquakes are retained"%(Nev,len(del_key_ev),len(ev_info)))

    return ev_info

# %%
# Function: limit_azi_gap(gap_thd) Calculate the azimuthal gap for all events and delete events with a gap greater than gap_thd.
def limit_azi_gap(gap_thd,ev_info,st_info):
    Nev = len(ev_info)

    del_key_ev = []
    for key_ev in ev_info:
        ev = ev_info[key_ev]
        gap = cal_azi_gap(ev,st_info)
        if (gap > gap_thd):
            del_key_ev.append(key_ev)
        else:
            ev_info[key_ev].tag["azi_gap"] = gap
    for key_ev in del_key_ev:
        del ev_info[key_ev]

    print("Original data set has %d earthquakes, %d earthquakes are deleted, %d earthquakes are retained"%(Nev,len(del_key_ev),len(ev_info)))

    return ev_info

# Function: cal_azi_gap(ev, st_info) Calculate the azimuthal gap of a single earthquake.
def cal_azi_gap(ev,st_info):
    azi_all = []
    lat_ev = ev.lat
    lon_ev = ev.lon
    stlist = {}
    for key in ev.t:
        stname = ev.t[key][0]
        if (not stname in stlist):
            lat_st = st_info[stname].lat
            lon_st = st_info[stname].lon
            azi = cal_azimuth(lat_ev, lon_ev, lat_st, lon_st)
            azi_all.append(azi)
            stlist[stname] = 1

    azi_all.sort()
    if(len(azi_all) < 2):
        return 360.0
    else:
        gap = 0.0
        for i in range(len(azi_all)-1):
            gap = max(gap,azi_all[i+1] - azi_all[i])
        gap = max(gap,azi_all[0] + 360 - azi_all[-1])
        return gap



# %%
# Function: limit_earthquake_decluster_Nt(ev_info, dlat, dlon, ddep, Top_N) Divide the region into several subdomains, sort by the number of arrival times, and retain only the top Top_N earthquakes with the most arrival times in each box.
# option 3, declustering. Divide the region into several subdomains, retain the Top N earthquakes in terms of the number of arrival times in each subdomain.
def limit_earthquake_decluster_Nt(ev_info,dlat,dlon,ddep,Top_N):
    # subdivide earthquakes into different subdomains
    [ev_info,tag2name] = tag_event_cluster(dlat,dlon,ddep,ev_info)

    # sort earthquakes in the same subdomain
    # Sort the quality of earthquakes within each tag based on the number of arrivals.
    tag2name = sort_cluster_Nt(ev_info, tag2name)

    # only retain Top_N earthquakes in each subdomain
    # Within each tag, prioritize selecting the top Top_N earthquakes.
    [ev_info,tag2name] = limit_decluster(ev_info, tag2name,Top_N)

    return ev_info



# Function: tag_event_cluster(size_lat, size_lon, size_dep, ev_info) Subdivide the study area, assign each earthquake to a subregion, and place it in a tag.
def tag_event_cluster(size_lat,size_lon,size_dep,ev_info):
    tag2name = {}
    for key_ev in ev_info:
        name = ev_info[key_ev].name
        lat = ev_info[key_ev].lat
        lon = ev_info[key_ev].lon
        dep = ev_info[key_ev].dep
        tag = "%d_%d_%d"%(math.floor(lon/size_lon),math.floor(lat/size_lat),math.floor(dep/size_dep))
        ev_info[key_ev].tag["cluster"] = tag

        if (tag in tag2name):
            tag2name[tag].append(name)
        else:
            tag2name[tag] = []
            tag2name[tag].append(name)

    return [ev_info,tag2name]

# Function: sort_cluster_Nt(ev_info, tag2name) Sort the quality of earthquakes within each tag based on the number of arrivals.
def sort_cluster_Nt(ev_info, tag2name):
    for key_tag in tag2name:
        names_ev = tag2name[key_tag]
        Nt = []
        for key_ev in names_ev:
            Nt.append(len(ev_info[key_ev].t))

        # Sort the earthquakes within each tag based on the number of arrivals.
        sorted_Nt = sorted(enumerate(Nt), key=lambda x: x[1], reverse=True)
        tag2name[key_tag] = []
        for index, Nt in sorted_Nt:
            tag2name[key_tag].append(names_ev[index])

    return tag2name

# Function: limit_cluster(ev_info, tag2name, Max) Prioritize selecting the top Max earthquakes within each tag.
def limit_decluster(ev_info, tag2name, Max):
    del_key_ev = []
    for key_tag in tag2name:
        names_ev = tag2name[key_tag]

        if(len(names_ev) > Max):
            tag2name[key_tag] = names_ev[0:Max]
            for i in range(Max,len(names_ev)):    # Delete earthquakes that exceed the threshold in the sorted list.
                del_key_ev.append(names_ev[i])

    for key_ev in del_key_ev:
        del ev_info[key_ev]

    return [ev_info,tag2name]



# %% [markdown]
# Functions: assign weights to earthquakes, stations, and data

# %%
# Function: box_weighting_ev(ev_info, dlat, dlon, ddep) Assign box-weight to the earthquakes.
def box_weighting_ev(ev_info,dlon,dlat,ddep):

    # categorization
    distribute = {}
    all_tag_wt = {}

    for key_ev in ev_info:
        lat_id = math.floor((ev_info[key_ev].lat) / dlat)
        lon_id = math.floor((ev_info[key_ev].lon) / dlon)
        dep_id = math.floor((ev_info[key_ev].dep) / ddep)

        tag = '%d_%d_%d'%(lat_id,lon_id,dep_id)
        if (tag in distribute):
            distribute[tag] += 1
        else:
            distribute[tag] = 1

    max_weight = 0
    for tag in distribute:
        all_tag_wt[tag] = 1.0/math.sqrt(distribute[tag])
        max_weight = max(max_weight,all_tag_wt[tag])

    for key_ev in ev_info:
        lat_id = math.floor((ev_info[key_ev].lat) / dlat)
        lon_id = math.floor((ev_info[key_ev].lon) / dlon)
        dep_id = math.floor((ev_info[key_ev].dep) / ddep)

        tag = '%d_%d_%d'%(lat_id,lon_id,dep_id)

        ev_info[key_ev].tag["weight"] = all_tag_wt[tag]/max_weight

    return ev_info

# %%
# Function: geographical_weighting_ev_rough(ev_info, dlat, dlon, ddep) Assign geographical weighting to the earthquakes roughly.
def geographical_weighting_ev_rough(ev_info,dlat,dlon,ddep,coefficient = 0.5):

    # categorization
    distribute = {}
    all_tag_wt = {}

    for key_ev in ev_info:
        lat_id = int(ev_info[key_ev].lat/dlat)
        lon_id = int(ev_info[key_ev].lon/dlat)
        dep_id = int(ev_info[key_ev].dep/ddep)


        tag = '%d_%d_%d'%(lat_id,lon_id,dep_id)
        if (tag in distribute):
            distribute[tag] += 1
        else:
            distribute[tag] = 1

    # Calculate the weight of each category.
    delta0 = 0
    for tag1 in distribute:
        tmp1 = tag1.split('_')
        # evlat1 = float(tmp1[0])*dlat; evlon1 = float(tmp1[1])*dlon; evdep1 = float(tmp1[2])*ddep

        for tag2 in distribute:
            tmp2 = tag2.split('_')
            # evlat2 = float(tmp2[0])*dlat; evlon2 = float(tmp2[1])*dlon; evdep2 = float(tmp2[2])*ddep

            # distance of id
            delta_tp = math.sqrt((int(tmp1[0]) - int(tmp2[0]))**2 + (int(tmp1[1]) - int(tmp2[1]))**2 + (int(tmp1[2]) - int(tmp2[2]))**2)
            delta0 = delta0 + distribute[tag1] * distribute[tag2] * delta_tp

    delta0 = delta0/(len(ev_info)**2) * coefficient

    max_weight = 0.0
    for tag1 in distribute:
        tmp1 = tag1.split('_')
        # evlat1 = float(tmp1[0])*dlat; evlon1 = float(tmp1[1])*dlon; evdep1 = float(tmp1[2])*ddep

        weight = 0
        for tag2 in distribute:
            tmp2 = tag2.split('_')
            # evlat2 = float(tmp2[0])*dlat; evlon2 = float(tmp2[1])*dlon; evdep2 = float(tmp2[2])*ddep

            delta_tp = math.sqrt((int(tmp1[0]) - int(tmp2[0]))**2 + (int(tmp1[1]) - int(tmp2[1]))**2 + (int(tmp1[2]) - int(tmp2[2]))**2)
            weight = weight + math.exp(-(delta_tp/delta0)**2) * distribute[tag2]

        all_tag_wt[tag1] = (1.0/weight)
        max_weight = max(max_weight,1.0/weight)

    # Assign weights to each earthquake based on its tag.
    for key_ev in ev_info:
        lat_id = int(ev_info[key_ev].lat/dlat)
        lon_id = int(ev_info[key_ev].lon/dlon)
        dep_id = int(ev_info[key_ev].dep/ddep)

        tag = '%d_%d_%d'%(lat_id,lon_id,dep_id)

        ev_info[key_ev].tag["weight"] = all_tag_wt[tag]/max_weight
    return ev_info


# %%
# Function: box_weighting_st(ev_info, st_info, dlat, dlon) Assign geographical weighting to the stations roughly.
def box_weighting_st(ev_info,st_info,dlon,dlat):

    [lon_ev,lat_ev,dep_ev,wt_ev] = data_lon_lat_dep_wt_ev(ev_info)

    # Integrate all involved stations.
    wt_st = {}
    name_st = {}
    for key_ev in ev_info:
        for key_t in ev_info[key_ev].t:
            name_rec = ev_info[key_ev].t[key_t][0]
            wt_st[name_rec] = -1.0
            name_st[name_rec] = 1

    # categorization
    distribute = {}
    all_tag_wt = {}

    # Count the number of stations in each subdomain.
    for key_st in name_st:
        lat_id = math.floor((st_info[key_st].lat) / dlat)
        lon_id = math.floor((st_info[key_st].lon) / dlon)

        tag = '%d_%d'%(lat_id,lon_id)
        if (tag in distribute):
            distribute[tag] += 1
        else:
            distribute[tag] = 1

    max_weight = 0
    for tag in distribute:
        all_tag_wt[tag] = 1.0/math.sqrt(distribute[tag])
        max_weight = max(max_weight,all_tag_wt[tag])

    # Assign weights to each station based on its tag.
    for key_st in name_st:
        lat_id = math.floor((st_info[key_st].lat) / dlat)
        lon_id = math.floor((st_info[key_st].lon) / dlon)
        tag = '%d_%d'%(lat_id,lon_id)
        wt_st[key_st] = all_tag_wt[tag]/max_weight

    # modify weight tag in st_info
    for key_t in wt_st:
        st_info[key_t].tag["weight"] = wt_st[key_t]

    # modify weight of abs data ev_info
    for key_ev in ev_info:
        for key_t in ev_info[key_ev].t:
            name_rec = ev_info[key_ev].t[key_t][0]
            ev_info[key_ev].t[key_t][3] = wt_st[name_rec]

    return [ev_info,st_info]


# %%
# Function: geographical_weighting_st(ev_info,st_info) Assign geographical weighting to the stations roughly.
def geographical_weighting_st(ev_info,st_info,coefficient = 0.5):

    # Integrate all involved stations.
    wt_st = {}
    name_st = {}
    for key_ev in ev_info:
        for key_t in ev_info[key_ev].t:
            name_rec = ev_info[key_ev].t[key_t][0]
            wt_st[name_rec] = -1.0
            name_st[name_rec] = 1

    # Calculate the weight of each station.
    delta0 = 0
    for key_st1 in name_st:
        stlat1 = st_info[key_st1].lat
        stlon1 = st_info[key_st1].lon

        for key_st2 in name_st:
            stlat2 = st_info[key_st2].lat
            stlon2 = st_info[key_st2].lon

            delta_tp = cal_dis(stlat1,stlon1,stlat2,stlon2)
            delta0 = delta0 + delta_tp

    delta0 = delta0/(len(wt_st)**2)*coefficient

    max_weight = 0.0
    for key_st1 in name_st:
        stlat1 = st_info[key_st1].lat
        stlon1 = st_info[key_st1].lon

        weight = 0
        for key_st2 in name_st:
            stlat2 = st_info[key_st2].lat
            stlon2 = st_info[key_st2].lon

            delta_tp = cal_dis(stlat1,stlon1,stlat2,stlon2)
            weight = weight + math.exp(-(delta_tp/delta0)**2)

        wt_st[key_st1] = (1.0/weight)
        max_weight = max(max_weight,1.0/weight)

    for key_st1 in wt_st:
        wt_st[key_st1] = wt_st[key_st1]/max_weight

    # Add weight to each data point in the earthquakes.
    for key_ev in ev_info:
        for key_t in ev_info[key_ev].t:
            name_rec = ev_info[key_ev].t[key_t][0]
            if (not name_rec in wt_st):
                ValueError("The station of the data is not in the calculation list")

            if (len(ev_info[key_ev].t[key_t])==3):
                ev_info[key_ev].t[key_t].append(wt_st[name_rec])
            elif (len(ev_info[key_ev].t[key_t])==4):
                ev_info[key_ev].t[key_t][3] = wt_st[name_rec]
            else:
                ValueError("Error in the weight information of the absolute traveltime data")

    # modify weight tag in st_info
    for key_t in wt_st:
        st_info[key_t].tag["weight"] = wt_st[key_t]

    # modify weight of abs data ev_info
    for key_ev in ev_info:
        for key_t in ev_info[key_ev].t:
            name_rec = ev_info[key_ev].t[key_t][0]
            ev_info[key_ev].t[key_t][3] = wt_st[name_rec]

    return [ev_info,st_info]


# %% [markdown]
# Function: add noise into data

# %%
# Function：assign_gaussian_noise():
def assign_gaussian_noise(ev_info,sigma):

    # Record which seismic phases correspond to each station.
    st2phase = {}   # Station name -> [Keys of absolute arrival time data related to this station]


    for key_ev in ev_info:
        # Absolute arrival time noise
        for key_t in ev_info[key_ev].t:
            stname = ev_info[key_ev].t[key_t][0]
            ev_info[key_ev].t[key_t][2] = ev_info[key_ev].t[key_t][2] + np.random.normal(0,sigma)
            if(stname in st2phase):
                st2phase[stname].append(key_t)
            else:
                st2phase[stname] = [key_t]

    for key_ev in ev_info:
        # Double-difference arrival time noise
        for key_dt in ev_info[key_ev].cs_dt:
            stname1 = ev_info[key_ev].cs_dt[key_dt][0]
            stname2 = ev_info[key_ev].cs_dt[key_dt][1]
            t1 = -999
            t2 = -999
            # Search for the arrival time of the data.
            if (stname1 in st2phase):
                for key_t in st2phase[stname1]:
                    if (key_t in ev_info[key_ev].t):
                        t1 = ev_info[key_ev].t[key_t][2]
                        break
            if (stname2 in st2phase):
                for key_t in st2phase[stname2]:
                    if (key_t in ev_info[key_ev].t):
                        t2 = ev_info[key_ev].t[key_t][2]
                        break

            if (t1 == -999 or t2 == -999):
                # If there is no absolute arrival time data, the double-difference data residuals increase by a factor of sqrt(2) in noise.
                ev_info[key_ev].cs_dt[key_dt][3] = ev_info[key_ev].cs_dt[key_dt][3] + np.random.normal(0,sigma*np.sqrt(2))
                print('no data: ', key_ev, key_dt)
            else:
                # If there is absolute arrival time data, the double-difference data is obtained by subtraction.
                ev_info[key_ev].cs_dt[key_dt][3] = t1 - t2

        # Common station double-difference arrival time
        for key_dt in ev_info[key_ev].cr_dt:
            stname  = ev_info[key_ev].cr_dt[key_dt][0]
            key_ev2 = ev_info[key_ev].cr_dt[key_dt][1]

            t1 = -999
            t2 = -999
            # Search for the arrival time of the data.
            if (stname in st2phase):
                for key_t in st2phase[stname]:
                    if (key_t in ev_info[key_ev].t):
                        t1 = ev_info[key_ev].t[key_t][2]
                        break
                    else:
                        print('not found 1: ', key_ev, key_t)

                for key_t in st2phase[stname]:
                    if (key_t in ev_info[key_ev2].t):
                        t2 = ev_info[key_ev2].t[key_t][2]
                        break
                    else:
                        print('not found 2: ', key_ev, key_t)

            if (t1 == -999 or t2 == -999):
                # If there is no absolute arrival time data, the double-difference data residuals increase by a factor of sqrt(2) in noise.
                ev_info[key_ev].cr_dt[key_dt][3] = ev_info[key_ev].cr_dt[key_dt][3] + np.random.normal(0,sigma*np.sqrt(2))
                print('no data: ', key_ev, key_dt)
            else:
                # If there is absolute arrival time data, the double-difference data is obtained by subtraction.
                ev_info[key_ev].cr_dt[key_dt][3] = t1 - t2

    return ev_info

# %%
# Function：assign_uniform_noise_to_ev():
def assign_uniform_noise_to_ev(ev_info, range_lat, range_lon, range_dep, range_time):

    # Loop through all earthquakes and assign noise to them.
    ev_noise = {}   # Name of the earthquake -> noise of [lat,lon,dep,ortime]
    # loop list of earthquakes
    for key_ev in ev_info:
        evname = key_ev
        if (evname in ev_noise):
            print("error: repeated earthquake name")
            exit()
        else:
            # generate noise
            ev_noise[evname] = np.random.uniform(-1,1,4) * np.array([range_lat,range_lon,range_dep,range_time])

    # Add noise to each data point.
    for key_ev in ev_info:

        # Absolute arrival time noise
        for key_t in ev_info[key_ev].t:

            ev_info[key_ev].t[key_t][2] = ev_info[key_ev].t[key_t][2] - ev_noise[key_ev][3]


        # Double-difference arrival time noise (double-difference arrival time remains unchanged)

        # Common station double-difference arrival time
        for key_dt in ev_info[key_ev].cr_dt:
            key_ev2 = ev_info[key_ev].cr_dt[key_dt][1]

            if (key_ev2 in ev_noise):
                ev_info[key_ev].cr_dt[key_dt][3] = ev_info[key_ev].cr_dt[key_dt][3] - ev_noise[key_ev][3] + ev_noise[key_ev2][3]
            else:
                print("earthquake %s is not included in ev_list"%(key_ev2))
                ev_noise[key_ev2] = np.random.uniform(-1,1,4) * np.array([range_lat,range_lon,range_dep,range_time])
                ev_info[key_ev].cr_dt[key_dt][3] = ev_info[key_ev].cr_dt[key_dt][3] - ev_noise[key_ev][3] + ev_noise[key_ev2][3]


    # Add noise to each earthquake.
    for key_ev in ev_noise:
        ev_info[key_ev].lat = ev_info[key_ev].lat + ev_noise[key_ev][0]
        ev_info[key_ev].lon = ev_info[key_ev].lon + ev_noise[key_ev][1]
        ev_info[key_ev].dep = abs(ev_info[key_ev].dep + ev_noise[key_ev][2])
        ev_info[key_ev].ortime = ev_info[key_ev].ortime + ev_noise[key_ev][3]


    return ev_info

# %% [markdown]
# Functions: generate differential traveltime

# %%
# Function: generate_cs_dif(ev_info, st_info, dis_thd, azi_thd) Generate double-difference arrival times from absolute arrival times, with inter-station distance less than dis_thd and azimuthal difference less than azi_thd.
# function: generate common source differential traveltime data from absolute traveltime data, the stations separation is less than dis_thd, the azimuth difference is less than azi_thd
def generate_cs_dif(ev_info,st_info,dis_thd,azi_thd):
    count_t = 0
    count_cs_dt = 0

    for key_ev in ev_info:
        ev = ev_info[key_ev]

        lat_ev = ev.lat
        lon_ev = ev.lon

        # traverse all arrival times
        name_st_list = []   # names of stations
        t_list = []         # traveltime
        wt_list = []        # weight
        for key_t in ev.t:
            name_st_list.append(ev.t[key_t][0])
            t_list.append(ev.t[key_t][2])
            wt_list.append(ev.t[key_t][3])
            count_t += 1

        # search for possible double-difference arrival times
        for id_st1 in range(len(name_st_list)-1):
            name_st1 = name_st_list[id_st1]
            lat_st1  = st_info[name_st1].lat
            lon_st1  = st_info[name_st1].lon
            t_st1    = t_list[id_st1]
            wt_st1   = wt_list[id_st1]

            for id_st2 in range(id_st1+1,len(name_st_list)):
                name_st2 = name_st_list[id_st2]
                lat_st2  = st_info[name_st2].lat
                lon_st2  = st_info[name_st2].lon
                t_st2    = t_list[id_st2]
                wt_st2   = wt_list[id_st2]

                dis = cal_dis(lat_st1,lon_st1,lat_st2,lon_st2)
                azi_st1 = cal_azimuth(lat_ev,lon_ev,lat_st1,lon_st1)
                azi_st2 = cal_azimuth(lat_ev,lon_ev,lat_st2,lon_st2)

                azi_dif = abs(azi_st1 - azi_st2)

                if(dis < dis_thd and (azi_dif < azi_thd or (360-azi_dif) < azi_thd )):
                    ev.cs_dt["%s+%s+%s"%(name_st1,name_st2,"P,cs")] = [name_st1,name_st2,"P,cs",t_st1-t_st2,(wt_st1+wt_st2)/2]
                    count_cs_dt += 1

        ev_info[key_ev].Ncs_dt = len(ev.cs_dt)

    print('we generate %d common source differential traveltimes from %s absolute traveltimes'%(count_cs_dt,count_t))
    return ev_info


# %%
# Function: generate_cr_dif(ev_info, st_info, dis_thd) Generate common station double-difference arrival times from absolute arrival times, with inter-event distance less than dis_thd.
# Function: generate common receiver differential traveltime data from absolute traveltime data, the earthquake separation is less than dis_thd
def generate_cr_dif(ev_info,st_info,dis_thd,azi_thd):

    # Construct mapping：rec2src[name_ev] -> {name_st: [name_ev, name_st, t, wt]; name_st: [name_ev, name_st, t, wt]; ...}
    rec2src = build_rec_src_map(ev_info,dis_thd)
    print("rec to src map generation finished")

    # Construct double-difference data association mapping：rec2src_pair[key_t]
    rec2src_pair = build_rec_src_pair_map(rec2src)
    print("rec to src_pair map generation finished")

    for key_t in rec2src_pair:
        name_st = key_t.split('+')[0]
        lat_st = st_info[name_st].lat
        lon_st = st_info[name_st].lon

        for ev_tag in rec2src_pair[key_t]:
            name_ev1 = rec2src_pair[key_t][ev_tag][0]
            lat_ev1  = ev_info[name_ev1].lat
            lon_ev1  = ev_info[name_ev1].lon
            dep_ev1  = ev_info[name_ev1].dep

            name_ev2 = rec2src_pair[key_t][ev_tag][1]
            lat_ev2  = ev_info[name_ev2].lat
            lon_ev2  = ev_info[name_ev2].lon
            dep_ev2  = ev_info[name_ev2].dep

            dis_xy = cal_dis(lat_ev1,lon_ev1,lat_ev2,lon_ev2)
            dis_z  = abs(dep_ev1 - dep_ev2)
            dis = math.sqrt(dis_xy**2 + dis_z**2)
            if(dis > dis_thd):      # limit of the distance between two earthquakes
                continue

            azi1 = cal_azimuth(lat_ev1,lon_ev1,lat_st,lon_st)
            azi2 = cal_azimuth(lat_ev2,lon_ev2,lat_st,lon_st)
            azi_dif = abs(azi1 - azi2)

            if(azi_dif > azi_thd and (360-azi_dif) > azi_thd):     # limit of the azimuth difference between two earthquakes
                continue

            t_ev1 = ev_info[name_ev1].t[key_t][2]
            t_ev2 = ev_info[name_ev2].t[key_t][2]
            wt_ev1 = ev_info[name_ev1].t[key_t][3] * ev_info[name_ev1].tag["weight"]
            wt_ev2 = ev_info[name_ev2].t[key_t][3] * ev_info[name_ev2].tag["weight"]
            # The actual data weight is wt_ev1 + wt_ev2, but in TomoATT calculations, we need to divide it by ev_info[name_ev1].tag["weight"].
            wt = (wt_ev1 + wt_ev2)/2/ev_info[name_ev1].tag["weight"]

            ev_info[name_ev1].cr_dt["%s+%s+%s"%(name_st,name_ev2,"P,cr")] = [name_st,name_ev2,"P,cr",t_ev1-t_ev2,wt]

    # Count the number of double-difference data points.
    count_cr_dt = 0
    count_t     = 0
    for key_ev in ev_info:
        ev_info[key_ev].Ncr_dt = len(ev_info[key_ev].cr_dt)
        count_cr_dt += ev_info[key_ev].Ncr_dt
        count_t     += ev_info[key_ev].Nt

    print('we generate %d common receiver differential traveltimes from %s absolute traveltimes'%(count_cr_dt,count_t))

    return ev_info

# Construct mapping: rec2src = {key_t: dict_tag; key_t: dict_tag; ...}
# dict_tag = {tag: list_name_ev; tag: list_name_ev; ...}
# list_name_ev = [name_ev1, name_ev2, ...]
# Assign earthquakes to different subregions based on their locations. The subregion size is dlat * dlon * ddep. When performing common station double-difference calculations, only earthquake pairs within the same subregion or adjacent subregions will be considered.
def build_rec_src_map(ev_info,dis_thd):
    rec2src = {}
    for key_ev in ev_info:
        name_ev = ev_info[key_ev].name
        lat = ev_info[key_ev].lat
        lon = ev_info[key_ev].lon
        dep = ev_info[key_ev].dep
        tag_dep = math.floor(dep/dis_thd)
        tag_lat = math.floor(lat/180*math.pi*R_earth/dis_thd)
        tag_lon = math.floor(lon/180*math.pi*R_earth*math.cos(lat)/dis_thd)
        tag = "%d_%d_%d"%(tag_lon,tag_lat,tag_dep)


        for key_t in ev_info[key_ev].t:

            # create dictionary
            if (not key_t in rec2src):
                rec2src[key_t] = {tag:[]}
            elif (not tag in rec2src[key_t]):
                rec2src[key_t][tag] = []

            # Add data
            rec2src[key_t][tag].append(name_ev)

    return rec2src

# Function: generate_adjacent_tag(tag) Generate tags surrounding the given tag.
def generate_adjacent_tag(tag): # Excluding the tag itself.
    adjacent_tag_list = []
    tmp = tag.split('_')
    tag_lon = int(tmp[0])
    tag_lat = int(tmp[1])
    tag_dep = int(tmp[2])

    for i in range(-1,2):
        for j in range(-1,2):
            for k in range(-1,2):
                if(i == 0 and j == 0 and k == 0):
                    continue
                adjacent_tag_list.append("%d_%d_%d"%(tag_lon+i,tag_lat+j,tag_dep+k))

    return adjacent_tag_list


# construct mapping：rec2src_pair
def build_rec_src_pair_map(rec2src):
    rec2src_pair = {}

    for key_t in rec2src:
        rec2src_pair[key_t] = {}

        for tag in rec2src[key_t]:
            name_ev_list1 = rec2src[key_t][tag]

            name_ev_list2 = rec2src[key_t][tag]
            adjacent_tag_list = generate_adjacent_tag(tag)
            for adjacent_tag in adjacent_tag_list:
                if (adjacent_tag in rec2src[key_t]):      # If the surrounding tag's region has earthquakes, add them to the earthquake list.
                    name_ev_list2 = name_ev_list2 + rec2src[key_t][adjacent_tag]

            # Find possible earthquake pairs.
            for id_ev1 in range(len(name_ev_list1)-1):
                name_ev1 = name_ev_list1[id_ev1]

                for id_ev2 in range(id_ev1+1,len(name_ev_list2)):   # Starting from id_ev1 + 1 already excludes duplicate earthquakes within the tag.
                    name_ev2 = name_ev_list2[id_ev2]

                    ev_tag1 = "%s+%s"%(name_ev1,name_ev2)
                    ev_tag2 = "%s+%s"%(name_ev2,name_ev1)

                    if(ev_tag1 in rec2src_pair[key_t] or ev_tag2 in rec2src_pair[key_t]):
                        continue

                    rec2src_pair[key_t][ev_tag1] = [name_ev1,name_ev2]


    return rec2src_pair

# %% [markdown]
# Functions: read and write src_rec.dat file

# %%
# Function: reorder_src(ev) Reorder the earthquake IDs. If the earthquake has no data, the ID is -999.
def reorder_src(ev_info):

    ev_id = 0
    for key_ev in ev_info:
        ev      = ev_info[key_ev]

        if(ev.Nt + ev.Ncs_dt + ev.Ncr_dt  == 0):
            ev.id = -999
        else:
            ev_info[key_ev].id = ev_id
            ev_id += 1

    return ev_info


# %%
# Function: read_src_rec_file(fname) Read the src_rec.dat file.
#
def read_src_rec_file(fname):
    ev_info = {}
    st_info = {}

    tmp_ev_info = {}

    doc = open(fname,'r')
    doc_input = doc.readlines()
    doc.close()

    cc = 0
    for info in doc_input:
        tmp=info.split()
        if (cc == 0):   # event line
            ev = Event()
            #  1 2000  1  2 20 28  37.270   38.2843     39.0241  11.00  3.60    8   1725385
            # id_ev   = int(tmp[0])
            ev.id   = int(tmp[0])
            year    = int(tmp[1])
            month   = int(tmp[2])
            day     = int(tmp[3])
            hour    = int(tmp[4])
            minute  = int(tmp[5])
            second  = float(tmp[6])
            ev.ortime = UTCDateTime(year,month,day,hour,minute,0) + second
            ev.lat  = float(tmp[7])
            ev.lon  = float(tmp[8])
            ev.dep  = float(tmp[9])
            ev.mag  = float(tmp[10])
            ev.Nt   = 0
            ev.Ncs_dt  = 0
            ev.Ncr_dt  = 0
            ev.t    = {}
            ev.cs_dt   = {}
            ev.cr_dt   = {}
            ndata   = int(tmp[11])
            name_ev = tmp[12]
            ev.name = name_ev
            cc += 1
            try:
                ev.tag["weight"] = float(tmp[13])
            except:
                pass

            if (ndata == 0):
                cc = 0
                ev_info[name_ev] = ev

        else:   # data line
            #  1      1      MYA       38.3261     38.4253   1050.0000  P   52.46   6.630 weight
            if(len(tmp) < 10):  # absolue traveltime data
                name_st = tmp[2]
                phase   = tmp[6]
                if (phase == "PG"):
                    phase = "Pg"
                if (phase == "PB"):
                    phase = "Pb"
                if (phase == "PN"):
                    phase = "Pn"

                if (not name_st in st_info):
                    st = Station()
                    st.name = name_st
                    st.id   = float(tmp[1])
                    st.lat  = float(tmp[3])
                    st.lon  = float(tmp[4])
                    st.ele  = float(tmp[5])
                    st_info[name_st] = st

                time    = float(tmp[7])
                if(len(tmp) == 9):
                    weight = float(tmp[8])
                else:
                    weight = 1.0
                ev.t["%s+%s"%(name_st,phase)] = [name_st,phase,time,weight]
                ev.Nt += 1

            else:   # differential traveltime data
                phase = tmp[11]
                if (phase.__contains__("cr")):  # common receiver differential traveltime
                    #  evid     stid1      stname1      lat1 lon1 eve1 evid2 evname2 lat2 lon2 dep2 phase,cr diftime weight

                    name_st1 = tmp[2]
                    if (not name_st1 in st_info):   # add station to the station list
                        st = Station()
                        st.name = name_st1
                        st.id   = float(tmp[1])
                        st.lat  = float(tmp[3])
                        st.lon  = float(tmp[4])
                        st.ele  = float(tmp[5])
                        st_info[name_st1] = st

                    name_ev2 = tmp[7]
                                                    # add earthquake to the temp earthquake list
                    ev2 = Event()
                    ev2.name = name_ev2
                    ev2.id   = float(tmp[6])
                    ev2.lat  = float(tmp[8])
                    ev2.lon  = float(tmp[9])
                    ev2.dep  = float(tmp[10])
                    tmp_ev_info[name_ev2] = ev2


                    dif_time = float(tmp[12])
                    if(len(tmp) == 14):
                        weight = float(tmp[13])
                    else:
                        weight = 1.0
                    ev.cr_dt["%s+%s+%s"%(name_st1,name_ev2,phase)] = [name_st1,name_ev2,phase,dif_time,weight]
                    ev.Ncr_dt += 1

                else:                           # common source differential traveltime
                    #  evid     stid1      stname1      lat1 lon1 eve1 stid2 stname2 lat2 lon2 ele2 phase,cs diftime weight

                    name_st1 = tmp[2]
                    if (not name_st1 in st_info):
                        st = Station()
                        st.name = name_st1
                        st.id   = len(st_info)
                        st.lat  = float(tmp[3])
                        st.lon  = float(tmp[4])
                        st.ele  = float(tmp[5])
                        st_info[name_st1] = st

                    name_st2 = tmp[7]
                    if (not name_st2 in st_info):
                        st = Station()
                        st.name = name_st2
                        st.id   = float(tmp[6])
                        st.lat  = float(tmp[8])
                        st.lon  = float(tmp[9])
                        st.ele  = float(tmp[10])
                        st_info[name_st2] = st

                    dif_time = float(tmp[12])
                    if(len(tmp) == 14):
                        weight = float(tmp[13])
                    else:
                        weight = 1.0
                    ev.cs_dt["%s+%s+%s"%(name_st1,name_st2,phase)] = [name_st1,name_st2,phase,dif_time,weight]
                    ev.Ncs_dt += 1

            if (cc == ndata):   # end of the event data
                cc = 0
                ev_info[name_ev] = ev
            else:
                cc += 1

    # Add earthquakes from the temporary earthquake list to the main earthquake list.
    for key_ev in tmp_ev_info:
        if (not key_ev in ev_info):
            ev_info[key_ev] = tmp_ev_info[key_ev]

    return [ev_info,st_info]

# %%
# Function: write_src_rec_file(fname, ev_info, st_info) Output the src_rec.dat file.
def write_src_rec_file(fname,ev_info,st_info):
    ev_info = reorder_src(ev_info)
    doc_src_rec = open(fname,'w')

    min_lat =  9999
    max_lat = -9999
    min_lon =  9999
    max_lon = -9999
    min_dep =  9999
    max_dep = -9999

    record_ev = {}
    record_st = {}
    Nt_total  = 0
    Ncs_dt_total = 0
    Ncr_dt_total = 0

    for key_ev in ev_info:
        ev      = ev_info[key_ev]
        evid    = ev.id
        year    = ev.ortime.year
        month   = ev.ortime.month
        day     = ev.ortime.day
        hour    = ev.ortime.hour
        minute  = ev.ortime.minute
        second  = ev.ortime.second
        msec    = ev.ortime.microsecond
        lat_ev  = ev.lat
        lon_ev  = ev.lon
        dep_ev  = ev.dep
        mag     = ev.mag
        ndata   = ev.Nt + ev.Ncs_dt + ev.Ncr_dt
        name_ev = ev.name
        try:
            weight_ev = ev.tag["weight"]
        except:
            weight_ev = 1.0

        if(ndata == 0):     # if the earthquake has no data, do not output it
            continue

        doc_src_rec.write('%7d %6d %2d %2d %2d %2d %5.2f %9.4f %9.4f %9.4f %5.2f %7d %s %7.3f\n'%(\
            evid,year,month,day,hour,minute,second+msec/1000000,lat_ev,lon_ev,dep_ev,mag,ndata,name_ev,weight_ev))

        min_lat =  min(min_lat, lat_ev)
        max_lat =  max(max_lat, lat_ev)
        min_lon =  min(min_lon, lon_ev)
        max_lon =  max(max_lon, lon_ev)
        min_dep =  min(min_dep, dep_ev)
        max_dep =  max(max_dep, dep_ev)

        record_ev[name_ev] = 1  # record this earthquake
        Nt_total += ev.Nt
        Ncs_dt_total += ev.Ncs_dt
        Ncr_dt_total += ev.Ncr_dt

        for key_t in ev.t:
            data    = ev.t[key_t]
            st      = st_info[data[0]]
            stid    = st.id
            name_st = st.name
            lat_st  = st.lat
            lon_st  = st.lon
            ele_st  = st.ele
            phase   = data[1]
            time    = data[2]
            try:
                weight_data = data[3]
            except:
                weight_data = 1.0
            doc_src_rec.write('%7d %7d %6s %9.4f %9.4f %9.4f %s %8.4f %7.3f \n'%(evid,stid,name_st,lat_st,lon_st,ele_st,phase,time,weight_data))

            min_lat =  min(min_lat, lat_st)
            max_lat =  max(max_lat, lat_st)
            min_lon =  min(min_lon, lon_st)
            max_lon =  max(max_lon, lon_st)
            min_dep =  min(min_dep, -ele_st/1000)
            max_dep =  max(max_dep, -ele_st/1000)

            record_st[name_st] = 1  # record this station

        for key_t in ev.cs_dt:
            data    = ev.cs_dt[key_t]
            st1     = st_info[data[0]]
            stid1   = st1.id
            name_st1= st1.name
            lat_st1 = st1.lat
            lon_st1 = st1.lon
            ele_st1 = st1.ele
            st2     = st_info[data[1]]
            stid2   = st2.id
            name_st2= st2.name
            lat_st2 = st2.lat
            lon_st2 = st2.lon
            ele_st2 = st2.ele
            phase   = data[2]
            time    = data[3]
            try:
                weight_data = data[4]
            except:
                weight_data = 1.0
            doc_src_rec.write('%7d %7d %6s %9.4f %9.4f %9.4f %7d %6s %9.4f %9.4f %9.4f %s %8.4f %7.3f \n'%(\
                evid,stid1,name_st1,lat_st1,lon_st1,ele_st1,stid2,name_st2,lat_st2,lon_st2,ele_st2,phase,time,weight_data))

            min_lat =  min(min_lat, lat_st1)
            max_lat =  max(max_lat, lat_st1)
            min_lon =  min(min_lon, lon_st1)
            max_lon =  max(max_lon, lon_st1)
            min_dep =  min(min_dep, -ele_st1/1000)
            max_dep =  max(max_dep, -ele_st1/1000)

            min_lat =  min(min_lat, lat_st2)
            max_lat =  max(max_lat, lat_st2)
            min_lon =  min(min_lon, lon_st2)
            max_lon =  max(max_lon, lon_st2)
            min_dep =  min(min_dep, -ele_st2/1000)
            max_dep =  max(max_dep, -ele_st2/1000)

            record_st[name_st1] = 1  # record this station
            record_st[name_st2] = 1  # record this station

        for key_t in ev.cr_dt:
            data    = ev.cr_dt[key_t]
            st      = st_info[data[0]]
            stid    = st.id
            name_st = st.name
            lat_st  = st.lat
            lon_st  = st.lon
            ele_st  = st.ele
            ev2     = ev_info[data[1]]
            evid2   = ev2.id
            name_ev2= ev2.name
            lat_ev2 = ev2.lat
            lon_ev2 = ev2.lon
            dep_ev2 = ev2.dep
            phase   = data[2]
            time    = data[3]
            try:
                weight_data = data[4]
            except:
                weight_data = 1.0
            doc_src_rec.write('%7d %7d %6s %9.4f %9.4f %9.4f %7d %6s %9.4f %9.4f %9.4f %s %8.4f %7.3f \n'%(\
                evid,stid,name_st,lat_st,lon_st,ele_st,evid2,name_ev2,lat_ev2,lon_ev2,dep_ev2,phase,time,weight_data))

            min_lat =  min(min_lat, lat_st)
            max_lat =  max(max_lat, lat_st)
            min_lon =  min(min_lon, lon_st)
            max_lon =  max(max_lon, lon_st)
            min_dep =  min(min_dep, -ele_st/1000)
            max_dep =  max(max_dep, -ele_st/1000)

            min_lat =  min(min_lat, lat_ev2)
            max_lat =  max(max_lat, lat_ev2)
            min_lon =  min(min_lon, lon_ev2)
            max_lon =  max(max_lon, lon_ev2)
            min_dep =  min(min_dep, dep_ev2)
            max_dep =  max(max_dep, dep_ev2)

            record_ev[name_ev2] = 1  # record this station
            record_st[name_st] = 1   # record this station

    doc_src_rec.close()

    print("src_rec.dat has been outputed: %d events, %d stations, %d abs traveltime, %d cs_dif traveltime, %d cr_dif traveltime. " \
          %(len(record_ev),len(record_st),Nt_total,Ncs_dt_total,Ncr_dt_total))
    print("earthquake and station region, lat: %6.1f - %6.1f, lon: %6.1f - %6.1f, dep: %6.1f - %6.1f"%(min_lat,max_lat,min_lon,max_lon,min_dep,max_dep)  )


# %%
# Function: write_src_list_file(fname, ev_info) Output the event list file.
def write_src_list_file(fname,ev_info):
    doc_ev_list = open(fname,'w')

    for key_ev in ev_info:
        ev      = ev_info[key_ev]
        evid    = ev.id
        lat_ev  = ev.lat
        lon_ev  = ev.lon
        dep_ev  = ev.dep
        mag     = ev.mag
        name_ev = ev.name
        if (ev.id == -999):     # if the earthquake has no data, do not output it
            continue
        doc_ev_list.write("%7d %s %s %9.4f %9.4f %9.4f %5.2f \n"%(evid,name_ev,ev.ortime,lat_ev,lon_ev,dep_ev,mag))
    doc_ev_list.close()

# %%
# Function: write_rec_list_file(fname, ev_info, st_info) Output the station list file.
def write_rec_list_file(fname,ev_info,st_info):
    doc_st_list = open(fname,'w')

    st_list = {}
    for key_ev in ev_info:
        ev      = ev_info[key_ev]

        for key_t in ev.t:
            data    = ev.t[key_t]
            st      = st_info[data[0]]
            name_st = st.name
            lat_st  = st.lat
            lon_st  = st.lon
            ele_st  = st.ele
            if(not name_st in st_list):
                doc_st_list.write("%6s %9.4f %9.4f %10.4f \n"%(name_st,lat_st,lon_st,ele_st))
                st_list[name_st] = 1

        for key_t in ev.cs_dt:
            data    = ev.cs_dt[key_t]
            st1     = st_info[data[0]]
            name_st1= st1.name
            lat_st1 = st1.lat
            lon_st1 = st1.lon
            ele_st1 = st1.ele
            st2     = st_info[data[0]]
            name_st2= st2.name
            lat_st2 = st2.lat
            lon_st2 = st2.lon
            ele_st2 = st2.ele
            if(not name_st1 in st_list):
                doc_st_list.write("%6s %9.4f %9.4f %10.4f \n"%(name_st1,lat_st1,lon_st1,ele_st1))
                st_list[name_st1] = 1
            if(not name_st2 in st_list):
                doc_st_list.write("%6s %9.4f %9.4f %10.4f \n"%(name_st2,lat_st2,lon_st2,ele_st2))
                st_list[name_st2] = 1

        for key_t in ev.cr_dt:
            data    = ev.cr_dt[key_t]
            st      = st_info[data[0]]
            name_st = st.name
            lat_st  = st.lat
            lon_st  = st.lon
            ele_st  = st.ele
            if(not name_st in st_list):
                doc_st_list.write("%6s %9.4f %9.4f %10.4f \n"%(name_st,lat_st,lon_st,ele_st))
                st_list[name_st] = 1

    doc_st_list.close()

# %% [markdown]
# Functions: read objective function file

# %%
# Function: read_objective_function_file(path)
def read_objective_function_file(path):

    full_curve = []
    location_curve = []
    model_curve = []

    with open('%s/objective_function.txt'%(path)) as f:
        for i,line in enumerate(f):
            tmp = line.split(',')
            if (tmp[0].__contains__("#")):
                continue   # skip the comment line
        
            iter        = int(tmp[0])
            tag         = tmp[1]
            obj         = float(tmp[2])
            obj_abs     = float(tmp[3])
            obj_cs      = float(tmp[4])
            obj_cr      = float(tmp[5])
            obj_tele    = float(tmp[6])
            tmp2        = tmp[7].split('/')
            mean        = float(tmp2[0])
            std         = float(tmp2[1])
            tmp2        = tmp[8].split('/')
            mean_abs    = float(tmp2[0])
            std_abs     = float(tmp2[1])
            tmp2        = tmp[9].split('/')
            mean_cs     = float(tmp2[0])
            std_cs      = float(tmp2[1])
            tmp2        = tmp[10].split('/')
            mean_cr     = float(tmp2[0])
            std_cr      = float(tmp2[1])
            tmp2        = tmp[11].split('/')
            mean_tele   = float(tmp2[0])
            std_tele    = float(tmp2[1])

            full_curve.append([obj,obj_abs,obj_cs,obj_cr,obj_tele,mean,std,mean_abs,std_abs,mean_cs,std_cs,mean_cr,std_cr,mean_tele,std_tele])
            if tag.__contains__("relocation"):
                location_curve.append([obj,obj_abs,obj_cs,obj_cr,obj_tele,mean,std,mean_abs,std_abs,mean_cs,std_cs,mean_cr,std_cr,mean_tele,std_tele])
            if tag.__contains__("model"):
                model_curve.append([obj,obj_abs,obj_cs,obj_cr,obj_tele,mean,std,mean_abs,std_abs,mean_cs,std_cs,mean_cr,std_cr,mean_tele,std_tele])

    return np.array(full_curve),np.array(location_curve),np.array(model_curve)

# %% [markdown]
# Functions: read inversion grid file

# %%
# Function: read the inversion grid file
def read_inversion_grid_file(path):

    inv_grid_vel = []
    inv_grid_ani = []

    switch = False
    igrid = -1
    with open('%s/inversion_grid.txt'%(path)) as f:
        tmp_inv_grid = []
        for i,line in enumerate(f):

            # read the number of inversion grid in dep, lat, lon directions
            if(i==0):
                tmp = line.split()
                ndep = int(tmp[1])
                nlines = 3*ndep+1   # The number of rows for each inversion grid is 3*ndep+1

            iline = i % nlines

            if(iline == 0):    # info: number of inversion grid
                tmp = line.split()
                if (int(tmp[0]) > igrid):
                    igrid = int(tmp[0])
                else:   # change from vel to ani
                    switch = True
                    igrid = int(tmp[0])

            else:               # info location of inversion grid
                iline_sub = (iline-1) % 3
                if(iline_sub == 0): # dep
                    tmp = line.split()
                    dep = float(tmp[0])
                if(iline_sub == 1): # list of lat
                    lat_list = line.split()
                if(iline_sub == 2): # list of lon
                    lon_list = line.split()

                    # add inversion grid
                    for lat in lat_list:
                        for lon in lon_list:
                            tmp_inv_grid.append([float(lon), float(lat), dep])

                if(iline == nlines-1): # the last line of inversion grid
                    if(switch):
                        inv_grid_ani.append(tmp_inv_grid)
                    else:
                        inv_grid_vel.append(tmp_inv_grid)
                    tmp_inv_grid = []

    return [np.array(inv_grid_vel),np.array(inv_grid_ani)]

# %% [markdown]
# Functions: for plotting

# %%
# Function: fig_ev_st_distribution_dep(ev_info, st_info) Plot the distribution of the earthquakes and stations, color-coded by earthquake depth.
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

def fig_ev_st_distribution_dep(ev_info,st_info):

    [lon_ev,lat_ev,dep_ev,wt_ev] = data_lon_lat_dep_wt_ev(ev_info)
    [lon_st,lat_st,ele_st,wt_st] = data_lon_lat_ele_wt_st(ev_info,st_info)

    min_lon = min(min(lon_ev),min(lon_st))
    max_lon = max(max(lon_ev),max(lon_st))

    min_lat = min(min(lat_ev),min(lat_st))
    max_lat = max(max(lat_ev),max(lat_st))

    max_dep = max(dep_ev)

    # Insert a value that does not affect the plot to make the colorbar range look better.
    lon_ev = np.insert(lon_ev, 0, 9999); lat_ev = np.insert(lat_ev, 0, 9999); dep_ev = np.insert(dep_ev, 0, 0);

    fig = plt.figure(figsize=(12,12))
    gridspace = GridSpec(12,12,figure = fig)

    xrange = max_lon - min_lon + 1.0
    yrange = max_lat - min_lat + 1.0

    if (xrange > yrange):
        fig_x_size = 6
        fig_y_size = round(6*yrange/xrange)
    else:
        fig_x_size = round(6*xrange/yrange)
        fig_y_size = 6


    ax1 = fig.add_subplot(gridspace[0:fig_y_size,0:fig_x_size])

    bar_ev = ax1.scatter(lon_ev,lat_ev,c=dep_ev,cmap="jet",label = "src",s = 3)
    # ax1.plot(lon_st,lat_st,'rv',label = "rec",markersize = 6)
    bar_st = ax1.scatter(lon_st,lat_st,c="red",label = "rec",s = 100,marker='v',edgecolors='white')

    ax1.legend(fontsize = 14)
    ax1.tick_params(axis='x',labelsize=18)
    ax1.tick_params(axis='y',labelsize=18)
    ax1.set_xlabel('Lon',fontsize=18)
    ax1.set_ylabel('Lat',fontsize=18)
    ax1.set_xlim((min_lon - (max_lon - min_lon)*0.1,max_lon + (max_lon - min_lon)*0.1))
    ax1.set_ylim((min_lat - (max_lat - min_lat)*0.1,max_lat + (max_lat - min_lat)*0.1))


    ax2 = fig.add_subplot(gridspace[0:fig_y_size, fig_x_size+1 : fig_x_size+3])

    ax2.scatter(dep_ev,lat_ev,c=dep_ev,cmap="jet",label = "src",s = 3)

    ax2.tick_params(axis='x',labelsize=18)
    ax2.tick_params(axis='y',labelsize=18)
    ax2.set_xlabel('Dep',fontsize=18)
    ax2.set_ylabel('Lat',fontsize=18)
    ax2.set_xlim((-max_dep*0.05,max_dep*1.1))
    ax2.set_ylim((min_lat - (max_lat - min_lat)*0.1,max_lat + (max_lat - min_lat)*0.1))


    ax3 = fig.add_subplot(gridspace[fig_y_size+1:fig_y_size+3,0:fig_x_size])

    ax3.scatter(lon_ev,dep_ev,c=dep_ev,cmap="jet",label = "src",s = 3)

    ax3.tick_params(axis='x',labelsize=18)
    ax3.tick_params(axis='y',labelsize=18)
    ax3.set_xlabel('Lon',fontsize=18)
    ax3.set_ylabel('Dep',fontsize=18)
    ax3.set_xlim((min_lon - (max_lon - min_lon)*0.1,max_lon + (max_lon - min_lon)*0.1))
    ax3.set_ylim((-max_dep*0.05,max_dep*1.1))
    ax3.invert_yaxis()

    # Place the colorbar on a new axis.
    ax4 = fig.add_subplot(gridspace[fig_y_size+2:fig_y_size+3,fig_x_size+1:fig_x_size+3])
    cbar1 = plt.colorbar(bar_ev, ax=ax4,orientation='horizontal')
    cbar1.set_label('Depth of earthquakes',fontsize=16)
    cbar1.ax.tick_params(axis='x', labelsize=16)    # Colorbar font size.

    # Hide the borders of the axes.
    ax4.spines['top'].set_visible(False)
    ax4.spines['right'].set_visible(False)
    ax4.spines['bottom'].set_visible(False)
    ax4.spines['left'].set_visible(False)

    # Hide the tick values of the axes.
    ax4.set_xticks([])
    ax4.set_yticks([])


# %%
# Function: fig_ev_st_distribution_wt(ev_info, st_info) Plot the distribution of the earthquakes and stations, color-coded by weight.
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
from matplotlib.colors import ListedColormap
from mpl_toolkits.axes_grid1 import make_axes_locatable
import math

def fig_ev_st_distribution_wt(ev_info,st_info):


    [lon_ev,lat_ev,dep_ev,wt_ev] = data_lon_lat_dep_wt_ev(ev_info)
    [lon_st,lat_st,ele_st,wt_st] = data_lon_lat_ele_wt_st(ev_info,st_info)

    # Insert a value that does not affect the plot to make the colorbar range look better.
    lon_ev = np.insert(lon_ev, 0, lon_ev[0]); lat_ev = np.insert(lat_ev, 0, lat_ev[0]); dep_ev = np.insert(dep_ev, 0, dep_ev[0]); wt_ev = np.insert(wt_ev, 0, 0.0)
    lon_ev = np.insert(lon_ev, 0, lon_ev[0]); lat_ev = np.insert(lat_ev, 0, lat_ev[0]); dep_ev = np.insert(dep_ev, 0, dep_ev[0]); wt_ev = np.insert(wt_ev, 0, 1.0)
    lon_st = np.insert(lon_st, 0, lon_st[0]); lat_st = np.insert(lat_st, 0, lat_st[0]); ele_st = np.insert(ele_st, 0, ele_st[0]); wt_st = np.insert(wt_st, 0, 0.0)
    lon_st = np.insert(lon_st, 0, lon_st[0]); lat_st = np.insert(lat_st, 0, lat_st[0]); ele_st = np.insert(ele_st, 0, ele_st[0]); wt_st = np.insert(wt_st, 0, 1.0)

    min_lon = min(min(lon_ev),min(lon_st))
    max_lon = max(max(lon_ev),max(lon_st))

    min_lat = min(min(lat_ev),min(lat_st))
    max_lat = max(max(lat_ev),max(lat_st))

    max_dep = max(dep_ev)

    fig = plt.figure(figsize=(12,12))
    gridspace = GridSpec(12,12,figure = fig)

    xrange = max_lon - min_lon + 1.0
    yrange = max_lat - min_lat + 1.0

    if (xrange > yrange):
        fig_x_size = 6
        fig_y_size = round(6*yrange/xrange)
    else:
        fig_x_size = round(6*xrange/yrange)
        fig_y_size = 6


    ax1 = fig.add_subplot(gridspace[0:fig_y_size,0:fig_x_size])

    bar_ev = ax1.scatter(lon_ev,lat_ev,c=wt_ev,cmap="jet",label = "src",s = 3)
    bar_st = ax1.scatter(lon_st,lat_st,c=wt_st,cmap="jet",label = "rec",s = 100,marker='^',edgecolors='white')

    ax1.legend(fontsize = 14)
    ax1.tick_params(axis='x',labelsize=18)
    ax1.tick_params(axis='y',labelsize=18)
    ax1.set_xlabel('Lon',fontsize=18)
    ax1.set_ylabel('Lat',fontsize=18)
    ax1.set_xlim((min_lon - (max_lon - min_lon)*0.1,max_lon + (max_lon - min_lon)*0.1))
    ax1.set_ylim((min_lat - (max_lat - min_lat)*0.1,max_lat + (max_lat - min_lat)*0.1))


    ax2 = fig.add_subplot(gridspace[0:fig_y_size, fig_x_size+1 : fig_x_size+3])

    ax2.scatter(dep_ev,lat_ev,c=wt_ev,cmap="jet",label = "src",s = 3)

    ax2.tick_params(axis='x',labelsize=18)
    ax2.tick_params(axis='y',labelsize=18)
    ax2.set_xlabel('Dep',fontsize=18)
    ax2.set_ylabel('Lat',fontsize=18)
    ax2.set_xlim((-max_dep*0.05,max_dep*1.1))
    ax2.set_ylim((min_lat - (max_lat - min_lat)*0.1,max_lat + (max_lat - min_lat)*0.1))


    ax3 = fig.add_subplot(gridspace[fig_y_size+1:fig_y_size+3,0:fig_x_size])

    ax3.scatter(lon_ev,dep_ev,c=wt_ev,cmap="jet",label = "src",s = 3)

    ax3.tick_params(axis='x',labelsize=18)
    ax3.tick_params(axis='y',labelsize=18)
    ax3.set_xlabel('Lon',fontsize=18)
    ax3.set_ylabel('Dep',fontsize=18)
    ax3.set_xlim((min_lon - (max_lon - min_lon)*0.1,max_lon + (max_lon - min_lon)*0.1))
    ax3.set_ylim((-max_dep*0.05,max_dep*1.1))
    ax3.invert_yaxis()

    # Place the colorbar on a new axis.
    ax4 = fig.add_subplot(gridspace[fig_y_size+2:fig_y_size+3,fig_x_size+1:fig_x_size+3])
    cbar1 = plt.colorbar(bar_st, ax=ax4,orientation='horizontal')
    cbar1.set_label('Weight of stations',fontsize=16)
    cbar1.ax.tick_params(axis='x', labelsize=16)    # colorbar font size.
    # Hide the borders of the axes.
    ax4.spines['top'].set_visible(False)
    ax4.spines['right'].set_visible(False)
    ax4.spines['bottom'].set_visible(False)
    ax4.spines['left'].set_visible(False)

    # Hide the tick values of the axes.
    ax4.set_xticks([])
    ax4.set_yticks([])

    # Place the colorbar on a new axis.
    ax5 = fig.add_subplot(gridspace[fig_y_size+1:fig_y_size+2,fig_x_size+1:fig_x_size+3])
    cbar1 = plt.colorbar(bar_ev, ax=ax5,orientation='horizontal')
    cbar1.set_label('Weight of earthquakes',fontsize=16)
    cbar1.ax.tick_params(axis='x', labelsize=16)    # colorbar font size.
    # Hide the borders of the axes.
    ax5.spines['top'].set_visible(False)
    ax5.spines['right'].set_visible(False)
    ax5.spines['bottom'].set_visible(False)
    ax5.spines['left'].set_visible(False)

    # Hide the tick values of the axes.
    ax5.set_xticks([])
    ax5.set_yticks([])


# %%
# Plot and function: plot the distance-time scatter plot, remove the outliers.
# Limit the data within the range defined by the line time = dis * slope + intercept and the bounds up and down.
# Remove outliers, only retain data satisfying: slope * dis + intercept + down < time < slope * dis + intercept + up.

def fig_data_plot_remove_outliers(ev_info,st_info,slope,intercept,up,down,dis_min,dis_max):

    fig = plt.figure(figsize=(10,10))
    gridspace = GridSpec(6,6,figure = fig)
    ax2 = fig.add_subplot(gridspace[0:6, 0:6])

    # plot original data
    [dis_obs,time_obs] = data_dis_time(ev_info,st_info)
    ax2.plot(dis_obs,time_obs,'r.',markersize=1.5,label = "discarded")

    # remove outliers, only retain data satisfying:     slope * dis + intercept + down < time < slope * dis + intercept + up
    ev_info = limit_data_residual(ev_info,st_info,slope,intercept,up,down)

    [dis_obs,time_obs] = data_dis_time(ev_info,st_info)
    ax2.plot(dis_obs,time_obs,'b.',markersize=1.5,label = "retained")

    ax2.plot([dis_min,dis_max],[slope*dis_min+intercept+up,slope*dis_max+intercept+up],'b-',linewidth=2)
    ax2.plot([dis_min,dis_max],[slope*dis_min+intercept+down,slope*dis_max+intercept+down],'b-',linewidth=2)
    ax2.plot([dis_min,dis_max],[slope*dis_min+intercept,slope*dis_max+intercept],'k-',linewidth=2)

    ax2.legend(fontsize = 14)
    ax2.tick_params(axis='x',labelsize=18)
    ax2.tick_params(axis='y',labelsize=18)
    ax2.set_xlabel('Distance (km)',fontsize=18)
    ax2.set_ylabel('Traveltime',fontsize=18)
    ax2.set_xlim((dis_min,dis_max))
    ax2.set_ylim((intercept+down-5,slope*dis_max+intercept+up+5))

    return ev_info

# %%
# Plot: distance-time scatter plot of given phases.

def fig_data_plot_phase(ev_info,st_info,phase_list,color_list,dis_min,dis_max):

    [dis_obs_phase,time_obs_phase] = data_dis_time_phase(ev_info,st_info,phase_list)

    regression = {}
    # Calculate the least squares y = ax+b
    for key_phase in phase_list:
        X = dis_obs_phase[key_phase]
        Y = time_obs_phase[key_phase]

        if(len(X)>20):
            regression[key_phase] = linear_regression(X,Y)
        else:
            print("No enough data: %d, for %s"%(len(X),key_phase))
            regression[key_phase] = [0,0,0]


    # draw
    fig = plt.figure(figsize=(10,10))
    gridspace = GridSpec(6,6,figure = fig)
    ax2 = fig.add_subplot(gridspace[0:6, 0:6])
    y1 = 99999; y2 = -99999

    # scatter plot
    for iphase in range(len(phase_list)):
        phase = phase_list[iphase]
        color = color_list[iphase]
        ax2.plot(dis_obs_phase[phase],time_obs_phase[phase],'%s.'%(color),markersize=1)

    # linear regression plot
    for iphase in range(len(phase_list)):
        phase = phase_list[iphase]
        color = color_list[iphase]
        (slope,intercept,SEE)= regression[phase]
        ax2.plot([dis_min,dis_max],[dis_min*slope+intercept,dis_max*slope+intercept],'%s-'%(color),linewidth=2,label = "%s: a,b,SEE=%5.2f,%5.2f,%5.2f"%(phase,slope,intercept,SEE))
        y1 = min(y1,intercept-5)
        y2 = max(y2,dis_max*slope+intercept+5)

    ax2.legend(fontsize = 14)
    ax2.tick_params(axis='x',labelsize=18)
    ax2.tick_params(axis='y',labelsize=18)
    ax2.set_xlabel('Distance (km)',fontsize=18)
    ax2.set_ylabel('Traveltime (s)',fontsize=18)
    ax2.set_xlim((dis_min,dis_max))


    for iphase in range(len(phase_list)):
        try:
            y1 = min(y1,min(time_obs_phase[phase]))
            y2 = max(y2,max(time_obs_phase[phase]))
        except:
            pass
    ax2.set_ylim((y1,y2))

    title = ""
    for phase in dis_obs_phase:
        title = title + "%s(%d) "%(phase,len(dis_obs_phase[phase]))

    ax2.set_title(title)

    print("a is slope, b is intercept, SEE is standard error of estimate")

# %% [markdown]
# Functions: results analysis and evaluation

# %%
# plot: plot the residual histogram of the initial and final model
def fig_residual_histogram(fn_syn_init,fn_syn_final,fn_obs,range_l,range_r,Nbar,tag1 = "initial",tag2 = "final"):

    # read synthetic traveltime data in the initial model
    [ev_info_syn_init, st_info_syn_init] = read_src_rec_file(fn_syn_init)
    time_syn_init = data_dis_time(ev_info_syn_init,st_info_syn_init)[1]

    # read synthetic traveltime data in the final model
    [ev_info_syn_final, st_info_syn_final] = read_src_rec_file(fn_syn_final)
    time_syn_final = data_dis_time(ev_info_syn_final,st_info_syn_final)[1]

    # read observed traveltime data
    [ev_info_obs, st_info_obs] = read_src_rec_file(fn_obs)
    time_obs = data_dis_time(ev_info_obs,st_info_obs)[1]

    fig = plt.figure(figsize=(6,6))
    gridspace = GridSpec(6,6,figure = fig)

    ax2 = fig.add_subplot(gridspace[0:6, 0:6])

    bins=np.linspace(range_l,range_r,Nbar)
    error_init = time_syn_init - time_obs
    error_final = time_syn_final - time_obs

    hist_init,  _, _ = ax2.hist(error_init,bins=bins,histtype='step', edgecolor = "red", linewidth = 2,
            label = "%s: std = %5.3f s, mean = %5.3f s"%(tag1,np.std(error_init),np.mean(error_init)))

    hist_final, _, _ = ax2.hist(error_final,bins=bins,alpha = 0.5, color = "blue",
            label = "%s: std = %5.3f s, mean = %5.3f s"%(tag2,np.std(error_final),np.mean(error_final)))

    print("residual for ",tag1," model is: ","mean: ",np.mean(error_init),"sd: ",np.std(error_init))
    print("residual for ",tag2," model is: ","mean: ",np.mean(error_final),"sd: ",np.std(error_final))
    ax2.legend(fontsize=14)

    ax2.set_xlim(range_l - abs(range_l)*0.1,range_r + abs(range_r)*0.1)
    ax2.set_ylim(0,1.3*max(max(hist_init),max(hist_final)))

    ax2.tick_params(axis='x',labelsize=18)
    ax2.tick_params(axis='y',labelsize=18)
    ax2.set_ylabel('Number of data',fontsize=18)
    ax2.set_xlabel('Traveltime residuals (s)',fontsize=18)
    ax2.set_title("$t_{syn} - t_{obs}$",fontsize=18)
    ax2.grid()


# %%
# plot: plot the residual histogram of the initial and final model
def fig_data_difference_histogram(fn_A,fn_B,range_l,range_r,Nbar):

    # read data A
    [ev_info_A, st_info_A] = read_src_rec_file(fn_A)

    # read data B
    [ev_info_B, st_info_B] = read_src_rec_file(fn_B)

    # absolute traveltime residual
    error_t = []
    for key_ev in ev_info_A:
        for key_t in ev_info_A[key_ev].t:
            data_A = ev_info_A[key_ev].t[key_t]
            if (key_ev in ev_info_B and key_t in ev_info_B[key_ev].t):
                data_B = ev_info_B[key_ev].t[key_t]
                error_t.append(data_A[2] - data_B[2])

    # common-source differential traveltime residual
    error_cs_dt = []
    for key_ev in ev_info_A:
        for key_dt in ev_info_A[key_ev].cs_dt:
            data_A = ev_info_A[key_ev].cs_dt[key_dt]
            if (key_ev in ev_info_B and key_dt in ev_info_B[key_ev].cs_dt):
                data_B = ev_info_B[key_ev].cs_dt[key_dt]
                error_cs_dt.append(data_A[3] - data_B[3])
            else:
                print(key_ev,key_dt)

    # common-receiver differential traveltime residual
    error_cr_dt = []
    for key_ev in ev_info_A:
        for key_dt in ev_info_A[key_ev].cr_dt:
            data_A = ev_info_A[key_ev].cr_dt[key_dt]
            if (key_ev in ev_info_B and key_dt in ev_info_B[key_ev].cr_dt):
                data_B = ev_info_B[key_ev].cr_dt[key_dt]
                error_cr_dt.append(data_A[3] - data_B[3])

    # plot
    fig = plt.figure(figsize=(14,6))
    gridspace = GridSpec(6,14,figure = fig)


    ax2 = fig.add_subplot(gridspace[0:6, 0:6])
    bins=np.linspace(range_l,range_r,Nbar)
    # hist_t,  _, _ = ax2.hist(error_t,bins=bins,histtype='step', edgecolor = "red", linewidth = 2,
    #         label = "noise: std = %5.3f s, mean = %5.3f s"%(np.std(error_t),np.mean(error_t)))
    hist_t, _, _ = ax2.hist(error_t,bins=bins,alpha = 0.5, color = "blue",
            label = "noise: std = %5.3f s, mean = %5.3f s"%(np.std(error_t),np.mean(error_t)))

    ax2.legend(fontsize=14)
    ax2.set_xlim(range_l - abs(range_l)*0.1,range_r + abs(range_r)*0.1)
    try:
        ax2.set_ylim(0,1.3*max(hist_t))
    except:
        ax2.set_ylim(0,1.0)
    ax2.tick_params(axis='x',labelsize=18)
    ax2.tick_params(axis='y',labelsize=18)
    ax2.set_ylabel('Number of data',fontsize=18)
    ax2.set_xlabel('Noise (s)',fontsize=18)
    ax2.set_title("Noise of traveltime",fontsize=18)


    ax3 = fig.add_subplot(gridspace[0:6,8:14])
    bins=np.linspace(range_l,range_r,Nbar)
    # hist_t,  _, _ = ax3.hist(error_t,bins=bins,histtype='step', edgecolor = "red", linewidth = 2,
    #         label = "noise: std = %5.3f s, mean = %5.3f s"%(np.std(error_t),np.mean(error_t)))
    hist_cs_dt, _, _ = ax3.hist(error_cs_dt,bins=bins,alpha = 0.5, color = "blue",
            label = "noise: std = %5.3f s, mean = %5.3f s"%(np.std(error_cs_dt),np.mean(error_cs_dt)))

    ax3.legend(fontsize=14)
    ax3.set_xlim(range_l - abs(range_l)*0.1,range_r + abs(range_r)*0.1)
    try:
        ax3.set_ylim(0,1.3*max(hist_cs_dt))
    except:
        ax3.set_ylim(0,1.0)
    ax3.tick_params(axis='x',labelsize=18)
    ax3.tick_params(axis='y',labelsize=18)
    ax3.set_ylabel('Number of data',fontsize=18)
    ax3.set_xlabel('Noise (s)',fontsize=18)
    ax3.set_title("Noise of differential traveltime",fontsize=18)


