Skip to content

Commit 622a552

Browse files
rad-eng-59GitHub Enterprise
authored andcommitted
Add a stand-alone "Doppler Estimator" Python module (#700)
- Add a python script "fir_filter_func.py" for FIR filter design including Chebyshev Equi-ripple filter under `isce3/signal` used for subbanding. - Add a python script "doppler_est_func.py" as a low-level general time-domain and time-freq-domain Doppler estimator under `isce3/signal` . - Add a dedicated end-to-end module "doppler_lut_from_raw.py" for generating doppler `LUT2d` from `Raw` object under `nisar/workflows`. - Add a python executable "gen_doppler_range_product.py" for generating NISAR Doppler HDF5 product from L0B product under `nisar/workflows`. This module simply uses "doppler_lut_from_raw". - Add a python test suite "fir_filter_func.py" - Add a python test suite "doppler_lut_from_raw.py" - Add a python test suite "gen_doppler_range_product.py" - Expand mask array to cover bad values such as NaN or Zero on top of TX gap regions in "doppler_lut_from_raw.py" - Augment "Raw.py", "Base.py", and "DataDecoder.py" and its test suite - Add a new optional arg to "cheby_equi_ripple_filter" to adjust filter length - Add correlator coefficient to the Doppler functions/products - Run autopep8
1 parent 59fe31c commit 622a552

File tree

16 files changed

+1866
-1
lines changed

16 files changed

+1866
-1
lines changed

python/packages/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ endforeach()
2727
set (list_of_exe
2828
nisar/workflows/crossmul.py
2929
nisar/workflows/focus.py
30+
nisar/workflows/gen_doppler_range_product.py
3031
nisar/workflows/geo2rdr.py
3132
nisar/workflows/geocode_insar.py
3233
nisar/workflows/gcov.py
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
from pybind_isce3.signal import *
2+
from .fir_filter_func import cheby_equi_ripple_filter
3+
from .doppler_est_func import (corr_doppler_est, sign_doppler_est,
4+
unwrap_doppler)
25
from . import point_target_info
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
"""
2+
Collection of functions for doppler centroid estimation.
3+
"""
4+
import functools
5+
import numbers
6+
import collections as cl
7+
import numpy as np
8+
from scipy import fft
9+
10+
11+
def corr_doppler_est(echo, prf, lag=1, axis=None):
12+
"""Estimate Doppler centroid based on complex correlator.
13+
14+
It uses the Correlation Doppler Estimator (CDE) approach
15+
proposed by [MADSEN1989]_
16+
17+
Parameters
18+
----------
19+
echo : np.ndarray(complex)
20+
1-D or 2-D numpy complex array
21+
prf : float
22+
Pulse-repetition frequency or sampling rate in the azimuth
23+
direction in (Hz).
24+
lag : int, default=1
25+
Lag of the correlator, a positive value.
26+
axis : None or int, optional
27+
Axis along which the correlator is performed.
28+
If None it will be the first axis.
29+
30+
Returns
31+
-------
32+
float
33+
Ambiguous Doppler centroid within [-0.5*prf, 0.5*prf]
34+
float
35+
Correlation coefficient, a value within [0, 1]
36+
37+
Raises
38+
------
39+
ValueError
40+
For bad input arguments
41+
TypeError
42+
If echo is not numpy array
43+
RuntimeError:
44+
Mismtach between lag and number of elements of echo used in correlator
45+
np.AxisError:
46+
Mismtach between axis value and echo dimension
47+
48+
See Also
49+
--------
50+
sign_doppler_est : Sign-Doppler estimator
51+
wavelen_diversity_doppler_est
52+
53+
References
54+
----------
55+
.. [MADSEN1989] S. Madsen, 'Estimating The Doppler Centroid of SAR Data',
56+
IEEE Transaction On Aerospace and Elect Sys, March 1989
57+
58+
"""
59+
if prf <= 0.0:
60+
raise ValueError('prf must be a positive value')
61+
if not isinstance(echo, np.ndarray):
62+
raise TypeError('echo must be a numpy array')
63+
if echo.ndim > 2:
64+
raise ValueError('Max dimension of echo must be 2')
65+
if lag < 1:
66+
raise ValueError('Lag must be equal or larger than 1')
67+
if axis is None:
68+
axis = 0
69+
else:
70+
if axis > (echo.ndim - 1):
71+
raise np.AxisError(
72+
f'axis {axis} is out of bound for dimenion {echo.ndim}')
73+
74+
if axis == 0:
75+
if echo.shape[0] < (lag + 1):
76+
raise RuntimeError(
77+
f'Not enough samples for correlator along axis {axis}')
78+
xcor_cmp = (echo[lag:] * echo[:-lag].conj()).mean()
79+
# get mag of product of auto correlations
80+
acor_mag = np.sqrt((abs(echo[lag:])**2).mean())
81+
acor_mag *= np.sqrt((abs(echo[:-lag])**2).mean())
82+
else:
83+
if echo.shape[1] < (lag + 1):
84+
raise RuntimeError(
85+
f'Not enough samples for correlator along axis {axis}')
86+
xcor_cmp = (echo[:, lag:] * echo[:, :-lag].conj()).mean()
87+
# get mag of product of auto correlations
88+
acor_mag = np.sqrt((abs(echo[:, lag:])**2).mean())
89+
acor_mag *= np.sqrt((abs(echo[:, :-lag])**2).mean())
90+
91+
# calculate correlation coefficient
92+
if acor_mag > 0:
93+
corr_coef = abs(xcor_cmp) / acor_mag
94+
else:
95+
corr_coef = 0.0
96+
97+
return prf / (2.0 * np.pi * lag) * np.angle(xcor_cmp), corr_coef
98+
99+
100+
def sign_doppler_est(echo, prf, lag=1, axis=None):
101+
"""Estimate Doppler centroid based on sign of correlator coeffs.
102+
103+
It uses Sign-Doppler estimator (SDE) approach proposed by [MADSEN1989]_
104+
105+
Parameters
106+
----------
107+
echo : np.ndarray(complex)
108+
1-D or 2-D numpy complex array
109+
prf : float
110+
Pulse-repetition frequency or sampling rate in the azimuth
111+
direction in (Hz).
112+
lag : int, default=1
113+
Lag of the correlator, a positive value.
114+
axis : None or int, optional
115+
Axis along which the correlator is perform.
116+
If None it will be the firsr axis.
117+
118+
Returns
119+
-------
120+
float
121+
Ambiguous Doppler centroid within [-0.5*prf, 0.5*prf]
122+
123+
Raises
124+
------
125+
ValueError
126+
For bad input arguments
127+
TypeError
128+
If echo is not numpy array
129+
RuntimeError:
130+
Mismtach between lag and number of elements of echo used in correlator
131+
np.AxisError:
132+
Mismtach between Axis value and echo dimension
133+
134+
See Also
135+
--------
136+
corr_doppler_est : Correlation Doppler Estimator (CDE)
137+
wavelen_diversity_doppler_est
138+
139+
References
140+
----------
141+
.. [MADSEN1989] S. Madsen, 'Estimating The Doppler Centroid of SAR Data',
142+
IEEE Transaction On Aerospace and Elect Sys, March 1989
143+
144+
"""
145+
if prf <= 0.0:
146+
raise ValueError('prf must be a positive value')
147+
if not isinstance(echo, np.ndarray):
148+
raise TypeError('echo must be a numpy array')
149+
if echo.ndim > 2:
150+
raise ValueError('Max dimension of echo must be 2')
151+
if lag < 1:
152+
raise ValueError('Lag must be equal or larger than 1')
153+
if axis is None:
154+
axis = 0
155+
else:
156+
if axis > (echo.ndim - 1):
157+
raise np.AxisError(
158+
f'axis {axis} is out of bound for dimenion {echo.ndim}')
159+
160+
sgn_i = _sgn(echo.real)
161+
sgn_q = _sgn(echo.imag)
162+
163+
if axis == 0:
164+
if echo.shape[0] < (lag + 1):
165+
raise RuntimeError(
166+
f'Not enough samples for correlator along axis {axis}')
167+
xcor_ii = (sgn_i[lag:] * sgn_i[:-lag]).mean()
168+
xcor_qq = (sgn_q[lag:] * sgn_q[:-lag]).mean()
169+
xcor_iq = (sgn_i[lag:] * sgn_q[:-lag]).mean()
170+
xcor_qi = (sgn_q[lag:] * sgn_i[:-lag]).mean()
171+
else:
172+
if echo.shape[1] < (lag + 1):
173+
raise RuntimeError(
174+
f'Not enough samples for correlator along axis {axis}')
175+
xcor_ii = (sgn_i[:, lag:] * sgn_i[:, :-lag]).mean()
176+
xcor_qq = (sgn_q[:, lag:] * sgn_q[:, :-lag]).mean()
177+
xcor_iq = (sgn_i[:, lag:] * sgn_q[:, :-lag]).mean()
178+
xcor_qi = (sgn_q[:, lag:] * sgn_i[:, :-lag]).mean()
179+
180+
r_sinlaw = np.sin(0.5 * np.pi * np.asarray([xcor_ii, xcor_qq,
181+
xcor_qi, -xcor_iq]))
182+
xcor_cmp = 0.5 * complex(r_sinlaw[:2].sum(), r_sinlaw[2:].sum())
183+
184+
return prf / (2.0 * np.pi * lag) * np.angle(xcor_cmp)
185+
186+
187+
def wavelen_diversity_doppler_est(echo, prf, samprate, bandwidth,
188+
centerfreq):
189+
"""Estimate Doppler based on wavelength diversity.
190+
191+
It uses slope of phase of range frequency along with single-lag
192+
time-domain correlator approach proposed by [BAMLER1991]_.
193+
194+
Parameters
195+
----------
196+
echo : np.ndarray(complex)
197+
2-D complex basebanded echo, azimuth by range in time domain.
198+
prf : float
199+
Pulse repetition frequency in (Hz)
200+
samprate : float
201+
Sampling rate in range , second dim, in (Hz)
202+
bandwidth : float
203+
RF/chirp bandiwdth in (Hz)
204+
centerfreq : float
205+
RF center frequency of chirp in (Hz)
206+
207+
Returns
208+
-------
209+
float
210+
Unambiguous Doppler centroid at center frequency in (Hz)
211+
212+
Raises
213+
------
214+
ValueError
215+
For bad input
216+
TypeError
217+
If echo is not numpy array
218+
219+
See Also
220+
--------
221+
corr_doppler_est : Correlation Doppler Estimator (CDE)
222+
sign_doppler_est : Sign-Doppler estimator (SDE)
223+
224+
References
225+
----------
226+
.. [BAMLER1991] R. Bamler and H. Runge, 'PRF-Ambiguity Resolving by
227+
Wavelength Diversity', IEEE Transaction on GeoSci and Remote Sensing,
228+
November 1991.
229+
230+
"""
231+
if prf <= 0:
232+
raise ValueError('PRF must be positive value!')
233+
if samprate <= 0:
234+
raise ValueError('samprate must be positive value!')
235+
if bandwidth <= 0 or bandwidth >= samprate:
236+
raise ValueError('badnwidth must be positive less than samprate!')
237+
if centerfreq <= 0.0:
238+
raise ValueError('centerfreq must be positive value!')
239+
if not isinstance(echo, np.ndarray):
240+
raise TypeError('echo must be a numpy array')
241+
if echo.ndim != 2:
242+
raise ValueError('echo must have two dimensions')
243+
num_azb, num_rgb = echo.shape
244+
if num_azb <= 2:
245+
raise ValueError('The first dimension of echo must be larger than 2')
246+
if num_rgb > 2:
247+
raise ValueError('The second dimension of echo must be larger than 2!')
248+
249+
# FFT along range
250+
nfft = fft.next_fast_len(num_rgb)
251+
echo_fft = fft.fft(echo, nfft, axis=1)
252+
253+
# one-lag correlator along azimuth
254+
az_corr = (echo_fft[1:] * echo_fft[:-1].conj()).mean(axis=0)
255+
256+
# Get the unwrapped phase of range spectrum within +/-bandwidth/2.
257+
df = samprate / nfft
258+
half_bw = 0.5 * bandwidth
259+
idx_hbw = nfft // 2 - int(half_bw / df)
260+
unwrap_phs_rg = np.unwrap(np.angle(fft.fftshift(az_corr)
261+
[idx_hbw: -idx_hbw])) # (rad)
262+
263+
# perform linear regression in range freq within bandwidth
264+
freq_bw = -half_bw + df * np.arange(nfft - 2 * idx_hbw)
265+
pf_coef = np.polyfit(freq_bw, unwrap_phs_rg, deg=1)
266+
267+
# get the doppler centroid at center freq based on slope
268+
dop_slope = prf / (2. * np.pi) * pf_coef[0]
269+
270+
return centerfreq * dop_slope
271+
272+
273+
@functools.singledispatch
274+
def unwrap_doppler(dop, prf):
275+
"""Unwrap doppler value(s)
276+
277+
Parameters
278+
----------
279+
dop : float or np.ndarray(float) or Sequence[float]
280+
Doppler centroid value(s) in (Hz)
281+
prf : float
282+
Pulse repetition frequency in (Hz).
283+
284+
Returns
285+
-------
286+
float or np.ndarray(float)
287+
Unwrapped Doppler values the same format as input in (Hz)
288+
289+
Raises
290+
------
291+
ValueError
292+
For non-positive prf
293+
TypeError:
294+
Bad data stype for dop
295+
296+
"""
297+
raise TypeError('Unsupported data type for doppler')
298+
299+
300+
@unwrap_doppler.register(numbers.Real)
301+
def _unwrap_doppler_scalar(dop: float, prf: float) -> float:
302+
"""Returns single doppler as it is"""
303+
if prf <= 0.0:
304+
raise ValueError('prf must be a positive value')
305+
return dop
306+
307+
308+
@unwrap_doppler.register(np.ndarray)
309+
def _unwrap_doppler_array(dop: np.ndarray, prf: float) -> np.ndarray:
310+
"""Unwrap doppler values stored as numpy array"""
311+
if prf <= 0.0:
312+
raise ValueError('prf must be a positive value')
313+
freq2phs = 2 * np.pi / prf
314+
phs2freq = 1.0 / freq2phs
315+
return phs2freq*np.unwrap(freq2phs * dop)
316+
317+
318+
@unwrap_doppler.register(cl.abc.Sequence)
319+
def _unwrap_doppler_sequence(dop: cl.abc.Sequence, prf: float) -> np.ndarray:
320+
"""Unwrap doppler values stored as Sequence """
321+
if prf <= 0.0:
322+
raise ValueError('prf must be a positive value')
323+
freq2phs = 2 * np.pi / prf
324+
phs2freq = 1.0 / freq2phs
325+
return phs2freq*np.unwrap(freq2phs * np.asarray(dop))
326+
327+
# List of helper functions
328+
329+
330+
def _sgn(x: np.ndarray) -> np.ndarray:
331+
"""Wrapper around numpy.sign.
332+
333+
It replaces zero values with one.
334+
335+
"""
336+
s = np.sign(x)
337+
s[s == 0] = 1
338+
return s

0 commit comments

Comments
 (0)