10 meter (Northing) x 5 meter (Easting) North America OPERA Coregistered Single Look Complex from Sentinel-1 provisional product
This dataset contains Level-2 OPERA provisional coregistered single-look-complex (CSLC) data from Sentinel-1 (S1). The data in this example are provisional, non-validated, geocoded CSLC-S1 data covering the 2019 Ridgecrest earthquake in California.
The OPERA project is generating geocoded burst-wise CSLC-S1 products over North America which includes USA and US Territories within 200 km from the US border, Canada, and all mainland countries from the southern US border down to and including Panama. Each pixel within a burst SLC is represented by a complex number and contains both the amplitude and phase information. The CSLC-S1 products are distributed over projected map coordinates using the Universal Transverse Mercator (UTM) projection with spacing in the X- and Y-directions of 5 m and 10 m, respectively. Each OPERA CSLC-S1 product is distributed as a HDF5 file following the CF-1.8 convention with separate groups containing the data raster layers, the low-resolution correction layers, and relevant product metadata.
For more information about the OPERA project and other products please visit our website at https://www.jpl.nasa.gov/go/opera .
Science Dataset (SDS) layer: Ridgecrest Earthquakes
Please refer to the OPERA Product Specification Document for details about the CSLC-S1 product.
Prepared by M. Grace Bato and K. Devlin
%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use: %reload_ext autoreload
import numpy as np
import glob
import h5py
import fsspec
import gc
from pyproj import Proj, CRS
import matplotlib.pyplot as plt
import shapely.wkt as wkt
import rasterio
from rasterio.crs import CRS
from rasterio.transform import from_origin
from rasterio import merge
import folium
from folium import plugins
from collections import namedtuple
import rioxarray
import matplotlib as mpl
import sys
sys.path.append('../../')
from src.cslc_utils import get_s3path, read_cslc, cslc_info, rasterWrite, custom_merge, colorize, getbasemaps, moving_window_mean
import warnings
warnings.filterwarnings('ignore')
OPERA CSLC-S1 products are provided on a burst-wise basis. Here, we stream the bursts covering the area of interest over the earthquake sequence rather than download them. The streaming saves the end-user storage space but this process requires higher memory (RAM) resource. Note that the burst_id
is consistent with the ESA published burst ID maps. To view/download the ESA burst ID maps, click here.
This notebook allows the user to choose between CSLC-S1 GAMMA version or CalVal version. Please select the desired version in the cell below.
version = 'calval' #uncomment to use CalVal version
#version = 'gamma' #uncomment to use GAMMA version
if version == 'gamma':
data_dir = 's3://opera-pst-rs-pop1/products/cslc_historical_20190701_to_20190718/'
burst_id = ['T071-151217-IW3',
'T071-151217-IW2',
'T071-151218-IW3',
'T071-151218-IW2',
'T071-151219-IW3',
'T071-151219-IW2',
'T071-151220-IW2']
# burst_id = ['T071-151217-IW2', 'T071-151218-IW2'] #Test for at least 2 burst_ids
else:
data_dir = 's3://opera-provisional-products/CSLC/Ridgecrest/Descending'
burst_id = ['t071_151217_iw3',
't071_151217_iw2',
't071_151218_iw3',
't071_151218_iw2',
't071_151219_iw3',
't071_151219_iw2',
't071_151220_iw2']
#burst_id = ['t071_151217_iw2', 't071_151218_iw2'] #Test for at least 2 burst_ids
date = ['20190704', '20190716']
save_fn = f'Ridgecrest_{date[1]}-{date[0]}_{burst_id[0][:4]}.tif'
burst_id
and date
and stream the CSLC-S1 data¶# example of reading before and after event dataset
id = burst_id[0]
if version == 'gamma':
path_h5 = get_s3path(data_dir, id, date[0])
before = read_cslc(path_h5,'gamma')
path_h5 = get_s3path(data_dir, id, date[1])
after = read_cslc(path_h5,'gamma')
else:
path_h5 = f'{data_dir}/{id}/{date[0]}/{id}_{date[0]}.h5'
before = read_cslc(path_h5)
path_h5 = f'{data_dir}/{id}/{date[1]}/{id}_{date[1]}.h5'
after = read_cslc(path_h5)
# Clean memory
del before, after, path_h5
Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151217_iw3/20190704/t071_151217_iw3_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151217_iw3/20190716/t071_151217_iw3_20190716.h5
An interferogram, $I$, given by the following equation:
$$ I_{1 2} = C_1 C_2 e^{j (\phi_1 - \phi_2)} $$can be formed by simply cross-multiplying the two geocoded OPERA CSLC-S1: cslc1 * np.conj(cslc2)
.
def generate_interferogram(date1, date2, burst_id, version='calval'):
"""
Generate wrapped interferogram.
Parameters:
- date1: CSLC date before the event.
- date2: CSLC date after the event.
- burst_id: CSLC burst id.
- version: CSLC product version gamma or calval.
Returns:
- interferogram: 2D numpy array of wrapped complex interferogram values.
- metadada: namedtuple with raster upper right lat,lon coordinates and spacing, bbox.
"""
if version == 'gamma':
reference = read_cslc(get_s3path(data_dir, burst_id, date1), 'gamma')
secondary = read_cslc(get_s3path(data_dir, burst_id, date2), 'gamma')
metadata = cslc_info(get_s3path(data_dir, burst_id, date1), 'gamma')
else:
reference = read_cslc(f'{data_dir}/{burst_id}/{date1}/{burst_id}_{date1}.h5')
secondary = read_cslc(f'{data_dir}/{burst_id}/{date2}/{burst_id}_{date2}.h5')
metadata = cslc_info(f'{data_dir}/{burst_id}/{date1}/{burst_id}_{date1}.h5')
# Calculate burst-wise interferogram
print(f'Calculate burst-wise interferogram: {date1}-{date2}, burst_id:{burst_id}')
interferogram = secondary * np.conj(reference)
mask = np.ma.masked_invalid(interferogram).mask
del reference, secondary
return interferogram, metadata
for id in burst_id:
ifg, metadata = generate_interferogram(date[0], date[1], id)
# Plot
fig, ax = plt.subplots(figsize=(10,3))
cax = ax.imshow(colorize(np.angle(ifg[::10,::10]), 'jet', -np.pi, np.pi), cmap='jet',
interpolation='nearest', origin='upper', extent=metadata.bbox, vmin=-np.pi, vmax=np.pi)
cbar = fig.colorbar(cax, orientation='vertical', fraction=0.01, pad=0.02)
cbar.set_ticks([-np.pi, 0., np.pi])
cbar.set_ticklabels([r'$-\pi$', '$0$', r'$\pi$'])
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.set_title(f'IFG_{date[1]}-{date[0]}_{id}',fontsize=12)
del fig, ax, cax, cbar
# Save each interferogram as GeoTiff
print(' Save interferogram as GeoTiff!\n')
transform = from_origin(metadata.x_coord[0],metadata.y_coord[0],
metadata.dx, np.abs(metadata.dy))
rasterWrite(f'ifg_{id}.tif', np.angle(ifg),
transform, metadata.epsg, dtype=rasterio.float32)
del ifg, transform
gc.collect()
Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151217_iw3/20190704/t071_151217_iw3_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151217_iw3/20190716/t071_151217_iw3_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151217_iw3 Save interferogram as GeoTiff! Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151217_iw2/20190704/t071_151217_iw2_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151217_iw2/20190716/t071_151217_iw2_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151217_iw2 Save interferogram as GeoTiff! Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151218_iw3/20190704/t071_151218_iw3_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151218_iw3/20190716/t071_151218_iw3_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151218_iw3 Save interferogram as GeoTiff! Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151218_iw2/20190704/t071_151218_iw2_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151218_iw2/20190716/t071_151218_iw2_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151218_iw2 Save interferogram as GeoTiff! Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151219_iw3/20190704/t071_151219_iw3_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151219_iw3/20190716/t071_151219_iw3_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151219_iw3 Save interferogram as GeoTiff! Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151219_iw2/20190704/t071_151219_iw2_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151219_iw2/20190716/t071_151219_iw2_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151219_iw2 Save interferogram as GeoTiff! Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151220_iw2/20190704/t071_151220_iw2_20190704.h5 Streaming: s3://opera-provisional-products/CSLC/Ridgecrest/Descending/t071_151220_iw2/20190716/t071_151220_iw2_20190716.h5 Calculate burst-wise interferogram: 20190704-20190716, burst_id:t071_151220_iw2 Save interferogram as GeoTiff!
69
rasterio
and save the file as GeoTiff¶# Merge GeoTiff files with rasterio
ifg_files = [(f'ifg_{id}.tif') for id in burst_id]
ifg_files_to_mosaic = []
for f in ifg_files:
src = rasterio.open(f)
ifg_files_to_mosaic.append(src)
# Write merged interferogram to GeoTiff
dest, output_transform=merge.merge(ifg_files_to_mosaic, method=custom_merge)
with rasterio.open(ifg_files[0]) as src:
out_meta = src.meta.copy()
out_meta.update({"driver": "GTiff",
"height": dest.shape[1],
"width": dest.shape[2],
"transform": output_transform})
with rasterio.open(save_fn, "w", **out_meta) as dest1:
dest1.write(dest)
dest1.close()
# print(dest.shape)
# Read the merged raster file
src = rioxarray.open_rasterio(save_fn)
merged_ifg = src.rio.reproject("EPSG:4326")[0] # Folium maps are in EPSG:4326
minlon,minlat,maxlon,maxlat = merged_ifg.rio.bounds()
# Downsample for folium visualization and attach RGBA color to numpy array
colored_merged_ifg = colorize(merged_ifg[::6][::6], 'jet', -np.pi, np.pi)
del src
matplotlib
¶# Define new bounding box
new_bbox = [minlon,maxlon,minlat,maxlat]
# Plot using Matplotlib
fig, ax = plt.subplots(figsize=[12,9])
cax = ax.imshow(colored_merged_ifg[::2, ::2], cmap='jet',
interpolation=None, origin='upper', extent=new_bbox,
vmin=-np.pi, vmax=np.pi)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.set_title(f'IFG_{date[1]}-{date[0]}_{burst_id[0][:4]}',fontsize=12)
fig.colorbar(cax,orientation='vertical',fraction=0.01,pad=0.02)
plt.show()
del fig, ax, cax
gc.collect()
44908
Coherence ($\gamma$) is a qualitative marker for the interferogram, ranging between 0 and 1. Changes in the acquisition geometry and scattering (e.g. vegetation) and large amounts of surface deformation (> 2 \pi radians) lead to decorrelation. The higher the decorrelation, the lower the coherence, as can be observed closest to the surface rupture. Coherence is computed using a n x n window on full-resolution data. The equation for coherence is below, where a is the first CSLC, b is the the second CSLC, * denotes the complex conjugate, and โจโฉ denotes a spatial average.
$$ \gamma = \frac{\left | \left \langle ab^{*} \right \rangle \right |}{\sqrt{\left \langle aa^{*} \right \rangle\left \langle bb^{*} \right \rangle}} $$def calculate_coherence_chunk(interferogram, window_size=5,
chunk_size=50, overlap_size=10):
"""
Calculate the coherence of a wrapped interferogram in a memory-efficient way by processing in chunks.
Parameters:
- interferogram: 2D numpy array of complex values representing the wrapped interferogram.
- window_size: Size of the window used for local averaging.
- chunk_size: Size of the chunks for processing.
- overlap_size: Number of overlapping pixels between adjacent chunks.
Returns:
- coherence: 2D numpy array of coherence values.
"""
rows, cols = interferogram.shape
coherence = np.zeros_like(interferogram, dtype=np.float32)
step_size = chunk_size - overlap_size
for i in range(0, rows, step_size):
for j in range(0, cols, step_size):
# Define chunk boundaries with overlap
start_i = max(i - overlap_size, 0)
start_j = max(j - overlap_size, 0)
end_i = min(i + chunk_size, rows)
end_j = min(j + chunk_size, cols)
# Extract chunk
chunk = interferogram[start_i:end_i, start_j:end_j]
# Calculate Chunk Coherence
chunk_coherence = np.clip(np.abs(moving_window_mean(chunk, window_size)), 0, 1)
# Define the region to update in the coherence array
coh_start_i = max(i, 0)
coh_start_j = max(j, 0)
coh_end_i = min(i + step_size, rows)
coh_end_j = min(j + step_size, cols)
chunk_start_i = coh_start_i - start_i
chunk_start_j = coh_start_j - start_j
chunk_end_i = chunk_start_i + (coh_end_i - coh_start_i)
chunk_end_j = chunk_start_j + (coh_end_j - coh_start_j)
# Store
chunk_slice = np.s_[chunk_start_i:chunk_end_i, chunk_start_j:chunk_end_j]
coherence[coh_start_i:coh_end_i, coh_start_j:coh_end_j] = chunk_coherence[chunk_slice]
# Mask nan values and zero values based on merged ifg file
nan_mask = np.isnan(interferogram)
zero_mask = interferogram == 0
coherence[nan_mask] = np.nan
coherence[zero_mask] = 0
return coherence
# Calculate the correlation/coherence
merged_ifg_cpx = np.exp(1j * np.nan_to_num(merged_ifg))
coh = calculate_coherence_chunk(merged_ifg_cpx, window_size=10,
chunk_size=500, overlap_size=20)
# Mask nan values and zero values based on merged ifg file
nan_mask = np.isnan(merged_ifg)
zero_mask = merged_ifg == 0
coh[nan_mask] = np.nan
coh[zero_mask] = 0
# Downsample for folium visualization and attach RGBA color to numpy array
colored_coh = colorize(coh[::4][::4], 'gray', 0., 1)
# Plot
fig, ax = plt.subplots(figsize=(12,9))
cax = ax.imshow(colored_coh, cmap='gray',interpolation='nearest',
origin='upper',extent=new_bbox, vmin=0., vmax=1.)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.set_title(f'COH_{date[1]}-{date[0]}_{burst_id[0][:4]}',fontsize=12)
fig.colorbar(cax,orientation='vertical',fraction=0.01,pad=0.02)
plt.show()
del fig, ax, cax, nan_mask, zero_mask, coh, merged_ifg_cpx,
gc.collect()
6011
# Overlay merged bursts onto a basemap.
# Initialize Folium basemap and define center of mapview
xmid =(new_bbox[0]+new_bbox[1])/2 ; ymid = (new_bbox[2]+new_bbox[3])/2
m = folium.Map(location=[ymid, xmid],
zoom_start=9, tiles='CartoDB positron',
control_scale=True, show=True)
# Add custom basemaps
basemaps = getbasemaps()
for basemap in basemaps:
basemaps[basemap].add_to(m)
# Overlay merged interferogram on a basemap
folium.raster_layers.ImageOverlay(colored_merged_ifg,
opacity=1.0,
bounds=[[new_bbox[2],new_bbox[0]],
[new_bbox[3],new_bbox[1]]],
interactive=False,
name='Interferogram',
show=True).add_to(m)
# Include coherence as a layer
folium.raster_layers.ImageOverlay(colored_coh,
opacity=0.6,
bounds=[[new_bbox[2],new_bbox[0]],
[new_bbox[3],new_bbox[1]]],
interactive=False,
name='Coherence',
show=True).add_to(m)
#layer Control
m.add_child(folium.LayerControl())
# Add fullscreen button
plugins.Fullscreen().add_to(m)
#Add inset minimap image
minimap = plugins.MiniMap(width=300, height=300)
m.add_child(minimap)
#Mouse Position
fmtr = "function(num) {return L.Util.formatNum(num, 3) + ' ยบ ';};"
plugins.MousePosition(position='bottomright',
separator=' | ',
prefix="Lat/Lon:",
lat_formatter=fmtr,
lng_formatter=fmtr).add_to(m)
#Display
m