Skip to content

Commit dfc028a

Browse files
add s1-etad prep module for testing S1-ETAD products (#73)
+ add s1_etad.py as a wrapper around the `s1etad` module from ESA to read / prepare ETA correction, given a `s1reader` burst object Co-authored-by: Seongsu Jeong <[email protected]>
1 parent d8f9e80 commit dfc028a

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed

src/s1reader/s1_etad.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""Wrapper of s1etad module to read S1-ETAD products."""
2+
import datetime
3+
import glob
4+
import os
5+
6+
import numpy as np
7+
from matplotlib import pyplot as plt
8+
from scipy.interpolate import RectBivariateSpline
9+
10+
try:
11+
import s1etad
12+
except ImportError:
13+
raise ImportError('Can NOT import s1etad (https://gitlab.com/s1-etad/s1-etad)!')
14+
15+
16+
17+
def get_eta_correction_from_slc_burst(slc_burst, eta_dir, corr_type='sum', include_tropo=True,
18+
resample=True, plot=False, verbose=True, unit='second'):
19+
"""Get the ETAD correction for one S1 burst.
20+
21+
Parameters
22+
----------
23+
slc_burst: Sentinel1BurstSlc
24+
Sentinel-1 burst object from sentinel1_reader
25+
eta_dir: str
26+
Sentinel-1 ETAD product directory
27+
corr_type: (list of) str
28+
Sentinel-1 ETAD correction type:
29+
sar, atm, sum, bistatic, doppler, fmrate, geodetic, ionospheric, tropospheric
30+
where sar = bistatic + doppler + fmrate
31+
atm = ionospheric + tropospheric
32+
sum = sar + atm + geodetic
33+
resample: bool
34+
resample the low resolution ETA to the full resample SLC size
35+
unit: str
36+
output ETA correction unit:
37+
pixel, second (meter is not recommended, as time info is more consistent and universal in SAR)
38+
39+
Returns:
40+
----------
41+
slc_rg_corr: np.ndarray in float32 in size of (lines, samples)
42+
S1 ETAD correction in range direction in the unit of meter or second
43+
slc_az_corr: np.ndarray in float32 in size of (lines, samples)
44+
S1 ETAD correction in azimuth direction in the unit of meter or second
45+
"""
46+
vprint = print if verbose else lambda *args, **kwargs: None
47+
if unit not in ['second', 'meter', 'pixel']:
48+
raise ValueError(f'Un-recognized input unit={unit}!')
49+
meter = (unit == 'meter')
50+
# When True, read s1etad product in meters
51+
# When False, read s1etad product in seconds [recommended]
52+
53+
# locate / read ETA burst
54+
eta_burst, eta = get_eta_burst_from_slc_burst(slc_burst, eta_dir, verbose=verbose)
55+
56+
# read ETA correction data
57+
corr_type = corr_type.lower()
58+
if isinstance(corr_type, list):
59+
corr_types = list(corr_type)
60+
elif corr_type == 'sar':
61+
corr_types = ['bistatic', 'doppler', 'fmrate']
62+
elif corr_type == 'atm':
63+
corr_types = ['ionospheric', 'tropospheric']
64+
else:
65+
corr_types = [corr_type]
66+
vprint(f'read correction data with type: {corr_type}')
67+
68+
eta_rg_corr = np.zeros((eta_burst.lines, eta_burst.samples), dtype=np.float32)
69+
eta_az_corr = np.zeros((eta_burst.lines, eta_burst.samples), dtype=np.float32)
70+
for corr_type in corr_types:
71+
correction = eta_burst.get_correction(corr_type, meter=meter)
72+
73+
if 'x' in correction.keys():
74+
scale = slc_burst.range_sampling_rate if unit == 'pixel' else 1.0
75+
eta_rg_corr += correction['x'] * scale
76+
77+
if 'y' in correction.keys():
78+
scale = 1.0 / slc_burst.azimuth_time_interval if unit == 'pixel' else 1.0
79+
eta_az_corr += correction['y'] * scale
80+
81+
if not include_tropo and any(x in corr_types for x in ['tropospheric', 'sum']):
82+
print('excluding tropospheric component from ETAD products in X direction')
83+
correction = eta_burst.get_correction('tropospheric')
84+
scale = slc_burst.range_sampling_rate if unit == 'pixel' else 1.0
85+
eta_rg_corr -= correction['x'] * scale
86+
87+
if resample or plot:
88+
# calculate ETA grid
89+
eta_az_start = (eta.min_azimuth_time - slc_burst.sensing_mid).total_seconds() + eta_burst.sampling_start['y']
90+
eta_rg_start = eta.min_range_time + eta_burst.sampling_start['x']
91+
eta_az_ax = eta_az_start + np.arange(eta_burst.lines) * eta_burst.sampling['y']
92+
eta_rg_ax = eta_rg_start + np.arange(eta_burst.samples) * eta_burst.sampling['x']
93+
94+
# calculate SLC grid
95+
slc_az_ax = np.arange(slc_burst.length) * slc_burst.azimuth_time_interval \
96+
+ (slc_burst.sensing_start - slc_burst.sensing_mid).total_seconds()
97+
slc_rg_ax = np.arange(slc_burst.width) / slc_burst.range_sampling_rate \
98+
+ slc_burst.slant_range_time
99+
100+
if resample:
101+
# resample ETA correction data to the SLC grid
102+
vprint('resampling the ETA correction data from ETA grid to SLC grid ...')
103+
rg_interp = RectBivariateSpline(eta_az_ax, eta_rg_ax, eta_rg_corr, kx=1, ky=1) # bi-linear
104+
az_interp = RectBivariateSpline(eta_az_ax, eta_rg_ax, eta_az_corr, kx=1, ky=1) # bi-linear
105+
slc_rg_corr = rg_interp(slc_az_ax, slc_rg_ax)
106+
slc_az_corr = az_interp(slc_az_ax, slc_rg_ax)
107+
108+
if plot:
109+
vprint('plot ETA correction data and grid')
110+
111+
# figure 1 - ETA corrections
112+
fig, axs = plt.subplots(nrows=2, ncols=1, figsize=[12, 6], sharex=True, sharey=True)
113+
for ax, corr, title in zip(axs, [eta_rg_corr, eta_az_corr], ['x', 'y']):
114+
im = ax.imshow(corr, aspect='auto', interpolation='nearest')
115+
fig.colorbar(im, ax=ax, shrink=0.8, location='right').set_label('pixel')
116+
ax.set_title(f'correction [{title}]')
117+
ax.set_ylabel('Azimuth [pixel]')
118+
axs[1].set_xlabel('Range [pixel]')
119+
fig.tight_layout()
120+
121+
# figure 2 - ETA & SLC grids [for comparison/checking]
122+
eta_box = np.asarray([
123+
(eta_rg_ax[0], eta_az_ax[0]),
124+
(eta_rg_ax[0], eta_az_ax[-1]),
125+
(eta_rg_ax[-1], eta_az_ax[-1]),
126+
(eta_rg_ax[-1], eta_az_ax[0]),
127+
(eta_rg_ax[0], eta_az_ax[0]),
128+
])
129+
slc_box = np.asarray([
130+
(slc_rg_ax[0], slc_az_ax[0]),
131+
(slc_rg_ax[0], slc_az_ax[-1]),
132+
(slc_rg_ax[-1], slc_az_ax[-1]),
133+
(slc_rg_ax[-1], slc_az_ax[0]),
134+
(slc_rg_ax[0], slc_az_ax[0]),
135+
])
136+
137+
fig, ax = plt.subplots(figsize=[12, 3])
138+
ax.plot(eta_box[:, 0] * 1e3, eta_box[:, 1], 'C0', label='ETA')
139+
ax.plot(slc_box[:, 0] * 1e3, slc_box[:, 1], 'C1', label='SLC')
140+
ax.set_xlabel('Range [ms]')
141+
ax.set_ylabel('Azimuth [s]')
142+
ax.set_title('grid')
143+
ax.legend()
144+
ax.grid()
145+
146+
plt.show()
147+
148+
if not resample:
149+
return eta_rg_corr, eta_az_corr
150+
151+
return slc_rg_corr, slc_az_corr
152+
153+
154+
155+
def get_eta_burst_from_slc_burst(slc_burst, eta_dir, verbose=True):
156+
"""Read ETA burst corresponding to the input SLC burst."""
157+
158+
# locate ETAD file
159+
eta_file = get_eta_file_from_slc_burst(slc_burst, eta_dir, verbose=verbose)
160+
161+
# read ETA file using s1-etad
162+
eta = s1etad.Sentinel1Etad(eta_file)
163+
164+
# locate the ETA burst
165+
t0_query = slc_burst.sensing_start - datetime.timedelta(seconds=0.25)
166+
t1_query = slc_burst.sensing_stop + datetime.timedelta(seconds=0.25)
167+
if verbose:
168+
print(f'search ETA burst in {slc_burst.swath_name} with the following time range:')
169+
print(f'start time: {t0_query}')
170+
print(f'end time: {t1_query}')
171+
172+
selection = eta.query_burst(
173+
swath=slc_burst.swath_name.upper(),
174+
first_time=t0_query,
175+
last_time=t1_query,
176+
)
177+
178+
if len(selection) == 0:
179+
raise ValueError('No ETA burst found!')
180+
elif len(selection) > 1:
181+
raise ValueError('More than 1 ETA burst found, please adjust your search/query criteria!')
182+
183+
eta_burst = eta[slc_burst.swath_name.upper()][selection.bIndex.values[0]]
184+
185+
return eta_burst, eta
186+
187+
188+
def get_eta_file_from_slc_burst(slc_burst, eta_dir, verbose=True):
189+
"""Get/locate ETAD file path based on SLC burst."""
190+
191+
# safe filename --> ETA filename pattern
192+
fparts = os.path.basename(slc_burst.safe_filename).split('_')
193+
eta_fbase = f'{fparts[0]}_IW_ETA__*_{fparts[5]}_{fparts[6]}_{fparts[7]}_{fparts[8]}_*.SAFE'
194+
195+
# search the ETA filename pattern
196+
eta_file = glob.glob(os.path.join(eta_dir, eta_fbase))[0]
197+
if verbose:
198+
print(f'search ETA file with pattern: {eta_fbase}')
199+
print(f'locate ETA file: {eta_file}')
200+
201+
return eta_file
202+

0 commit comments

Comments
 (0)