Toy example of Ambient Noise Adjoint Tomography
This is a toy example of Ambient Noise Adjoint Tomography (ANAT) using the SpecFWAT package. It demonstrates how to set up a simple 3-D model, generate synthetic data, and perform the inversion process.
Access this example at SpecFWAT/tests/toy_example_noise .
The Jupyter notebook for preparation of this example can be downloaded here.
Define study region and mesh
The ./DATA/meshfem3D_files/Mesh_Par_file
is used to define the study region and mesh parameters. The file contains parameters such as latitude, longitude, depth, and UTM projection zone.
LATITUDE_MIN = -1.0
LATITUDE_MAX = 1.0
LONGITUDE_MIN = -1.0
LONGITUDE_MAX = 1.0
DEPTH_BLOCK_KM = 32.0
UTM_PROJECTION_ZONE = 30
SUPPRESS_UTM_PROJECTION = .false.
In this example, we define a 2x2 degree area centered at the origin with a depth of 32 km. The UTM projection zone is set to 30.
Generate initial and target model
Here we create a simple 1-D model as the initial model and 3-D model with a 2x2x2 checkerboard pattern as the target model. The model is defined in a 3-D grid.
import numpy as np
from pyfwat.pario import readpar, chpar
from pyproj import Proj
import h5py
import matplotlib.pyplot as plt
import pandas as pd
The pyfwat
is a Python package for preparation and post-processing of SpecFWAT data and model. It provides functions to read, write and change parameters.
Generate regular grid on UTM coordinates
This example use UTM coordinates due to SUPPRESS_UTM_PROJECTION
set to .false.
. Thus, we need to convert geographic coordinates (latitude and longitude) to UTM coordinates using the pyproj
library. We then create a regular grid with a spacing of 2 km in the horizontal direction and 1 km in the vertical direction.
# read mesh parameters
lat_min = readpar('./DATA/meshfem3D_files/Mesh_Par_file', 'LATITUDE_MIN')
lat_max = readpar('./DATA/meshfem3D_files/Mesh_Par_file', 'LATITUDE_MAX')
lon_min = readpar('./DATA/meshfem3D_files/Mesh_Par_file', 'LONGITUDE_MIN')
lon_max = readpar('./DATA/meshfem3D_files/Mesh_Par_file', 'LONGITUDE_MAX')
dep_max = readpar('./DATA/meshfem3D_files/Mesh_Par_file', 'DEPTH_BLOCK_KM')
# Get geographic coordinates and convert to UTM
zmax = 0
zmin = -dep_max*1000
zone = int(readpar('./DATA/meshfem3D_files/Mesh_Par_file', 'UTM_PROJECTION_ZONE'))
geo2utm = Proj(proj='utm', zone=zone, ellps='WGS84')
xmin1, ymin1 = geo2utm(lon_min, lat_min)
xmin2, ymin2 = geo2utm(lon_min, lat_max)
xmin = min(xmin1, xmin2)
xmax1, ymax1 = geo2utm(lon_max, lat_min)
xmax2, ymax2 = geo2utm(lon_max, lat_max)
xmax = max(xmax1, xmax2)
xmin1, ymin1 = geo2utm(lon_min, lat_min)
xmin2, ymin2 = geo2utm(lon_max, lat_min)
ymin = min(ymin1, ymin2)
xmax1, ymax1 = geo2utm(lon_min, lat_max)
xmax2, ymax2 = geo2utm(lon_max, lat_max)
ymax = max(ymax1, ymax2)
# Create axis for regular grid
dh = 2000
x = np.arange(xmin, xmax+dh, dh)
y = np.arange(ymin, ymax+dh, dh)
dz = 1000
z = np.arange(zmin, zmax+dz, dz)
Create initial model
First, We define a function to compute the P-wave velocity (vp
) and density (rho
) from the S-wave velocity (vs
).
def get_vp_rho(vs):
vp = 0.9409 + 2.0947*vs - 0.8206*vs**2 + 0.2683*vs**3 - 0.0251*vs**4
rho = 1.6612*vp - 0.4721*vp**2 + 0.0671*vp**3 - 0.0043*vp**4 + 0.000106*vp**5
# vs = 0.7858 - 1.2344*vp + 0.7949*vp**2 - 0.1238*vp**3 + 0.0064*vp**4
return vp, rho
Then we create a 1-D initial model with a linear increase of S-wave velocity from 3.3 km/s at the surface to 4.0 km/s at 27 km depth and a constant S-wave velocity of 4.0 km/s below 27 km depth.
vsmin, vsmax = 3.3, 4.0
vs = np.zeros((len(x), len(y), len(z)))
idx = np.where(z >= -27000)[0]
vs[:, :, idx] = np.linspace(vsmin, vsmax, len(idx))[::-1][np.newaxis, np.newaxis, :]
vs[:, :, z < -27000] = vsmax
vp, rho = get_vp_rho(vs)
Finally, we save the initial model to an HDF5 file: initial_model.h5
.
with h5py.File('initial_model.h5', 'w') as f:
f.create_dataset('x', data=x)
f.create_dataset('y', data=y)
f.create_dataset('z', data=z)
f.create_dataset('vp', data=vp*1000)
f.create_dataset('vs', data=vs*1000)
f.create_dataset('rho', data=rho*1000)
Create target model
We create a 3-D checkerboard pattern for the target model by adding 2x2x2 perturbations to the initial model. The perturbations are defined as a sine function in the horizontal direction and a cosine function in the vertical direction.
# create checkerboard model
limh = 40000
limz = [-30000, 0]
n_pert_x = 2
n_pert_y = 2
n_pert_z = 2
pertv = 0.12
x_pert = np.zeros_like(x)
ntaper_left, ntaper_right = create_taper(x[0]+limh, x[-1]-limh, x)
x_pert[ntaper_left:x.size-ntaper_right] = \
np.sin(n_pert_x*np.pi*np.arange(x.size-(ntaper_left+ntaper_right))/ \
(x.size-(ntaper_left+ntaper_right)))
print('size of anomaly:', (x[-1]-x[0]-2*limh)/n_pert_x)
y_pert = np.zeros_like(y)
ntaper_left, ntaper_right = create_taper(y[0]+limh, y[-1]-limh, y)
y_pert[ntaper_left:y.size-ntaper_right] = \
np.sin(n_pert_y*np.pi*np.arange(y.size-(ntaper_left+ntaper_right))/ \
(y.size-(ntaper_left+ntaper_right)))
print('size of anomaly:', (y[-1]-y[0]-2*limh)/n_pert_y)
z_pert = np.zeros_like(z)
ntaper_left, ntaper_right = create_taper(limz[0], limz[1], z)
z_pert[ntaper_left:z.size-ntaper_right] = \
np.sin(n_pert_z*np.pi*np.arange(z.size-(ntaper_left+ntaper_right))/ \
(z.size-(ntaper_left+ntaper_right)))
print('size of anomaly:', (limz[1]-limz[0])/n_pert_z)
xx, yy, zz = np.meshgrid(x_pert, y_pert, z_pert, indexing='ij')
pert = xx*yy*zz*pertv
vs_pert = vs*(1+pert)
vp_pert, rho_pert = get_vp_rho(vs_pert)
Finally, we save the target model to an HDF5 file: target_model.h5
.
with h5py.File('target_model.h5', 'w') as f:
f.create_dataset('x', data=x)
f.create_dataset('y', data=y)
f.create_dataset('z', data=z)
f.create_dataset('vp', data=vp_pert*1000)
f.create_dataset('vs', data=vs_pert*1000)
f.create_dataset('rho', data=rho_pert*1000)
Setup sources and receivers
We set up 49 receivers in a 7x7 grid with a spacing of 0.5 degrees and 9 sources in a 3x3 grid with a spacing of 1 degree. The sources and receivers are defined in UTM coordinates and then geographic coordinates for Specfem3D format.
Setup receiver and source template
lim = 20000
nrec = 7
nsrc = 3
sta = []
utm2geo = Proj(proj='latlong', zone=zone, datum='WGS84')
for j, sty in enumerate(np.linspace(ymin+lim, ymax-lim, nrec)):
for i, stx in enumerate(np.linspace(xmin+lim, xmax-lim, nrec)):
stnm = 'ST{:03d}'.format(j*nrec+i+1)
stlo, stla = geo2utm(stx, sty, inverse=True)
sta.append([stnm, 'MX', stla, stlo, 0.0, 0.0])
sta = pd.DataFrame(sta, columns=['stnm', 'net', 'lat', 'lon', 'elev', 'buried'])
src = []
ratio = nrec//nsrc+1
for j, sy in enumerate(np.linspace(ymin+lim, ymax-lim, nsrc)):
for i, sx in enumerate(np.linspace(xmin+lim, xmax-lim, nsrc)):
evnm = 'ST{:03d}'.format(j*ratio*nrec+i*ratio+1)
evlo, evla = geo2utm(sx, sy, inverse=True)
src.append([f'MX.{evnm}', evla, evlo, 0.0, 0.0, 1.0])
src = pd.DataFrame(src, columns=['event_name', 'event_lat', 'event_lon', 'event_depth', 'event_buried', 'event_weight'])
Save receivers for each source
We save the receivers for each source in a separate file in the src_rec/STATIONS_${event_name}
. See Sources and receivers section for details.
for i, row in src.iterrows():
staname = row['event_name'].split('.')[1]
sta_copy = sta[sta['stnm'] != staname]
sta_copy.to_csv(f'src_rec/STATIONS_{row['event_name']}', sep=' ', index=False, header=False)
Save source list
We save the source list in the src_rec/sources_noise.dat
file. See Source list section for details.
src.to_csv('src_rec/sources_noise.dat', sep=' ', index=False, header=False)
Write FORCESOLUTION
file
The FORCESOLUTION
file is used to specify the source time function and source locations for Specfem3D. We create a file FORCESOLUTION_${event_name}
in the src_rec
folder with the source information. See Source files section for details.
# write FORCESOLUTION files
solution = '''
FORCE 001
time shift: 0.0000
f0: 0.5
latorUTM: 0.0
longorUTM: 0.0
depth: 0.0000
source time function: 0
factor force source: 1.d15
component dir vect source E: 0.d0
component dir vect source N: 0.d0
component dir vect source Z_UP: 1.d0
'''
for i, row in src.iterrows():
with open(f'src_rec/FORCESOLUTION_{row["event_name"]}', 'w') as f:
solution_ev = chpar(solution, 'latorUTM', row['event_lat'], type='solution')
solution_ev = chpar(solution_ev, 'longorUTM', row['event_lon'], type='solution')
f.write(solution_ev)
Visualize the target model
We can visualize the target models and source and receiver locations using matplotlib
. Below is an example of 2-D slice at 20 km.
dep = -20000
iz = np.argmin(np.abs(z - dep))
plt.figure()
plt.pcolor(x, y, pert[:, :, iz].T, cmap='bwr_r')
plt.colorbar(label='perturbation')
plt.gca().set_aspect('equal')
stxx, styy = geo2utm(sta.lon, sta.lat)
plt.plot(stxx, styy, 'ro')
evxx, evyy = geo2utm(src.event_lon, src.event_lat)
plt.plot(evxx, evyy, 'bo')
Generate synthetic data
To generate synthetic data, we need to run the forward simulation using SpecFWAT. Run 00_forward.sh
to generate synthetic data for the target model. The synthetic data is saved in the fwat_data
folder.
#!/bin/bash
set -e
mkdir -p DATABASES_MPI
mkdir -p OUTPUT_FILES
NPROC=`grep ^NPROC DATA/Par_file | grep -v -E '^[[:space:]]*#' | cut -d = -f 2 | cut -d \# -f 1`
cp target_model.h5 DATA/tomo_files/tomography_model.h5
# Run Forward to generate data
mpirun -np $NPROC ../../bin/xfwat_mesh_databases -s noise
mpirun -np $NPROC ../../bin/xfwat_fwd_measure_adj -m M00 -s noise -r 1
Conduct the inversion
Run 01_run_this_test.sh
to perform the inversion process with 6 iterations.
#!/bin/bash
set -e
NPROC=`grep ^NPROC DATA/Par_file | grep -v -E '^[[:space:]]*#' | cut -d = -f 2 | cut -d \# -f 1`
# Run Inversion
for it in `seq 0 5`; do
MODEL=`printf "M%02d" $it`
if [ $it -eq 0 ]; then
cp initial_model.h5 DATA/tomo_files/tomography_model.h5
fi
mpirun -np $NPROC ../../bin/xfwat_mesh_databases -s noise
mpirun -np $NPROC ../../bin/xfwat_fwd_measure_adj -m $MODEL -s noise -r 3
mpirun -np $NPROC ../../bin/xfwat_post_proc -m $MODEL
mpirun -np $NPROC ../../bin/xfwat_optimize -m $MODEL
done