s1-denoise 0.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- s1_denoise/__init__.py +18 -0
- s1_denoise/__main__.py +36 -0
- s1_denoise/auxiliary_files.py +481 -0
- s1_denoise/baq_decoder.py +203 -0
- s1_denoise/cli.py +118 -0
- s1_denoise/error_calculation.py +262 -0
- s1_denoise/jo_parser.py +295 -0
- s1_denoise/l0_noise_power_calculation.py +399 -0
- s1_denoise/logger.py +81 -0
- s1_denoise/noise_norm_factor.py +291 -0
- s1_denoise/noise_profiles.py +292 -0
- s1_denoise/output.py +551 -0
- s1_denoise/s1safe.py +1415 -0
- s1_denoise/thermal_noise.py +753 -0
- s1_denoise/utils.py +282 -0
- s1_denoise/xml_reader.py +571 -0
- s1_denoise-0.0.0.dev0.dist-info/METADATA +147 -0
- s1_denoise-0.0.0.dev0.dist-info/RECORD +21 -0
- s1_denoise-0.0.0.dev0.dist-info/WHEEL +4 -0
- s1_denoise-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- s1_denoise-0.0.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module implementing the ThermalNoise class for physics-based simulation of
|
|
6
|
+
Sentinel-1 thermal noise profiles.
|
|
7
|
+
|
|
8
|
+
Encapsulates the full noise modelling chain: noise power (from the L0N product or
|
|
9
|
+
the annotation), elevation antenna pattern, range spreading loss, PG correction,
|
|
10
|
+
processing gain, and noise normalization factor, the latter reconstructed from the
|
|
11
|
+
azimuth antenna pattern on the azimuth FFT grid. Supports both SLC and GRD processing
|
|
12
|
+
for TopSAR IW and EW modes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import math
|
|
17
|
+
import sys
|
|
18
|
+
from numpy.typing import NDArray
|
|
19
|
+
from scipy.constants import speed_of_light
|
|
20
|
+
|
|
21
|
+
from s1_denoise import R_REF
|
|
22
|
+
from s1_denoise.utils import db_to_linear, remove_outliers
|
|
23
|
+
from s1_denoise.logger import denoise_logger
|
|
24
|
+
from s1_denoise.auxiliary_files import AuxiliaryFiles
|
|
25
|
+
from s1_denoise.xml_reader import (
|
|
26
|
+
read_pg_correction_list,
|
|
27
|
+
read_noise_power_correction_from_annotation,
|
|
28
|
+
read_antenna_patterns,
|
|
29
|
+
closest_antenna_pattern_index,
|
|
30
|
+
extract_FR_info,
|
|
31
|
+
extract_AR_info,
|
|
32
|
+
build_orbit
|
|
33
|
+
)
|
|
34
|
+
from s1_denoise.noise_norm_factor import (
|
|
35
|
+
find_fft_size,
|
|
36
|
+
hamming,
|
|
37
|
+
calculate_window_norm_factor,
|
|
38
|
+
update_window_noise_norm,
|
|
39
|
+
get_start_indices,
|
|
40
|
+
get_look_overlap_samples
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
class ThermalNoise:
|
|
44
|
+
"""
|
|
45
|
+
Thermal noise model of a single subswath and polarization of a Sentinel-1 L1 product.
|
|
46
|
+
|
|
47
|
+
Reads once all the burst-independent quantities (annotation parameters, auxiliary file values, antenna
|
|
48
|
+
patterns, PG corrections, orbit, noise power) and provides the methods to compute the simulated noise
|
|
49
|
+
range vector at a given azimuth time.
|
|
50
|
+
|
|
51
|
+
Before calling `compute_simulated_noise`, the caller must set the slant range axis with `set_range_values`,
|
|
52
|
+
set `rel_burst` and compute the shrink factor with `get_shrink_factor`.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
metadata_dict : dict
|
|
57
|
+
Product annotation parsed into a dictionary (see `xml_reader.read_metadata_from_safe_annotation`).
|
|
58
|
+
channel : S1Channel
|
|
59
|
+
Channel (polarization and swath) of the SAFE product, providing the parsed annotation XML tree.
|
|
60
|
+
aux_info : AuxiliaryFiles
|
|
61
|
+
Auxiliary files (AUX_PP1, AUX_CAL, AUX_INS) used for processing.
|
|
62
|
+
product_id : str
|
|
63
|
+
Product identifier used to select the AUX_PP1 parameters (e.g. ``"IW_SLC__1"``).
|
|
64
|
+
noise_power_from_l0 : dict, optional
|
|
65
|
+
Noise power extracted from the L0N product, mapping ``(swath, polarization)`` to
|
|
66
|
+
``[azimuth_times, noise_powers]``. If None (default), the noise power is read from the noise list of
|
|
67
|
+
the product annotation.
|
|
68
|
+
subswath : str, optional
|
|
69
|
+
Subswath identifier (e.g. ``"IW1"``) for GRD products, where the annotation covers all subswaths.
|
|
70
|
+
If None (default), the product is treated as SLC and the swath of `channel` is used.
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
is_grd : bool
|
|
75
|
+
True for GRD products, i.e. when `subswath` is given.
|
|
76
|
+
swath_id : str
|
|
77
|
+
Identifier of the processed subswath.
|
|
78
|
+
polarization : str
|
|
79
|
+
Polarization of the channel.
|
|
80
|
+
lines_per_burst : int
|
|
81
|
+
Number of lines per burst (1300 if annotated as 0).
|
|
82
|
+
samples_per_burst : int
|
|
83
|
+
Number of samples per burst.
|
|
84
|
+
look_bandwidth, processing_bandwidth : float
|
|
85
|
+
Azimuth look and processing bandwidths [Hz].
|
|
86
|
+
number_of_looks : int
|
|
87
|
+
Number of azimuth looks.
|
|
88
|
+
azi_window_coefficient : float
|
|
89
|
+
Azimuth Hamming window coefficient.
|
|
90
|
+
rg_processing_bandwidth : float
|
|
91
|
+
Range processing bandwidth [Hz].
|
|
92
|
+
radar_frequency : float
|
|
93
|
+
Radar carrier frequency [Hz].
|
|
94
|
+
velocities : list of float
|
|
95
|
+
Norm of the annotated orbit state vector velocities [m/s].
|
|
96
|
+
antenna_patterns : dict
|
|
97
|
+
Antenna pattern records of the subswath (see `xml_reader.read_antenna_patterns`).
|
|
98
|
+
pg_correction_factors : numpy.ndarray
|
|
99
|
+
Annotated PG product amplitudes.
|
|
100
|
+
pg_az_t : numpy.ndarray
|
|
101
|
+
Azimuth times of the PG correction factors, as PreciseDateTime.
|
|
102
|
+
noise_az_t : numpy.ndarray
|
|
103
|
+
Azimuth times of the retained noise power values.
|
|
104
|
+
noise_power_correction_factors : numpy.ndarray
|
|
105
|
+
Noise power values retained after the noise pulse discard.
|
|
106
|
+
outlier_detected : list
|
|
107
|
+
Discarded noise pulses, as ``[index, azimuth_time, noise_power]``.
|
|
108
|
+
range_over_sampling_factor : float
|
|
109
|
+
Ratio between the range sampling frequency after decimation and the range total bandwidth.
|
|
110
|
+
rel_burst : int or None
|
|
111
|
+
Index of the current burst (SLC) or antenna pattern block (GRD).
|
|
112
|
+
range_t : numpy.ndarray or None
|
|
113
|
+
Slant range times of the current noise vector samples [s].
|
|
114
|
+
range_distances : numpy.ndarray or None
|
|
115
|
+
Slant range distances of the current noise vector samples [m].
|
|
116
|
+
range_spread_loss : numpy.ndarray or None
|
|
117
|
+
Range spreading loss correction $(R_{ref} / R)^4$.
|
|
118
|
+
shrink_factor : float
|
|
119
|
+
TopSAR azimuth shrink factor, set by `get_shrink_factor`.
|
|
120
|
+
noise_norm_factor : float
|
|
121
|
+
Noise normalization factor, set by `reconstruct_noise_norm_factor`.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, metadata_dict, channel, aux_info: AuxiliaryFiles, product_id, noise_power_from_l0 = None, subswath = None):
|
|
125
|
+
|
|
126
|
+
self.metadata_dict = metadata_dict
|
|
127
|
+
self.channel = channel
|
|
128
|
+
self.annotation = channel.annotation
|
|
129
|
+
self.product_id = product_id
|
|
130
|
+
|
|
131
|
+
if subswath is None:
|
|
132
|
+
self.is_grd = False
|
|
133
|
+
self.swath_id = channel.swath_id # SLC case
|
|
134
|
+
else:
|
|
135
|
+
self.is_grd = True
|
|
136
|
+
self.swath_id = subswath # GRD case
|
|
137
|
+
self.polarization = channel.pol
|
|
138
|
+
|
|
139
|
+
# load and parse L1 annotation file
|
|
140
|
+
# product info
|
|
141
|
+
product = self.metadata_dict["product"]
|
|
142
|
+
self.mode = product['adsHeader']['mode']
|
|
143
|
+
self.product_type = product['adsHeader']['productType']
|
|
144
|
+
self.lines_per_burst = int(product["swathTiming"]["linesPerBurst"])
|
|
145
|
+
if self.lines_per_burst == 0:
|
|
146
|
+
self.lines_per_burst = 1300
|
|
147
|
+
self.samples_per_burst = int(product["swathTiming"]["samplesPerBurst"])
|
|
148
|
+
self.sentinel_type = product['adsHeader']['missionId']
|
|
149
|
+
|
|
150
|
+
# image annotation
|
|
151
|
+
image_annotation = product["imageAnnotation"]
|
|
152
|
+
self.az_dim = int(image_annotation["imageInformation"]["numberOfLines"])
|
|
153
|
+
self.apply_azi_ant_pattern = str(image_annotation["processingInformation"]["antennaAzimuthPatternApplied"]).strip().lower() == "true"
|
|
154
|
+
self.input_azi_spacing = 1 / float(image_annotation["imageInformation"]["azimuthFrequency"])
|
|
155
|
+
|
|
156
|
+
# azimuth processing info
|
|
157
|
+
params = image_annotation["processingInformation"]["swathProcParamsList"]["swathProcParams"]
|
|
158
|
+
# normalize to list
|
|
159
|
+
if isinstance(params, dict):
|
|
160
|
+
params = [params]
|
|
161
|
+
elif not isinstance(params, list):
|
|
162
|
+
denoise_logger.error(
|
|
163
|
+
f"Unexpected swath processing parameters type: {type(params)}"
|
|
164
|
+
)
|
|
165
|
+
sys.exit(128)
|
|
166
|
+
|
|
167
|
+
self.swath_params = [item for item in params if item.get("swath") == self.swath_id]
|
|
168
|
+
|
|
169
|
+
azimuth_processing = self.get_azimuth_processing(self.swath_id)
|
|
170
|
+
self.look_bandwidth = float(azimuth_processing["lookBandwidth"])
|
|
171
|
+
self.processing_bandwidth = float(azimuth_processing["processingBandwidth"])
|
|
172
|
+
self.number_of_looks = int(azimuth_processing["numberOfLooks"])
|
|
173
|
+
self.look_overlap = float(azimuth_processing["lookOverlap"])
|
|
174
|
+
self.azi_window_coefficient = float(azimuth_processing["windowCoefficient"])
|
|
175
|
+
|
|
176
|
+
range_processing = self.get_range_processing(self.swath_id)
|
|
177
|
+
self.rg_processing_bandwidth = float(range_processing["processingBandwidth"])
|
|
178
|
+
|
|
179
|
+
# general annotation
|
|
180
|
+
self.radar_frequency = float(product["generalAnnotation"]["productInformation"]["radarFrequency"])
|
|
181
|
+
self.velocities = []
|
|
182
|
+
self.orbits = product["generalAnnotation"]["orbitList"]["orbit"]
|
|
183
|
+
|
|
184
|
+
for orbit in self.orbits:
|
|
185
|
+
velocity = orbit["velocity"]
|
|
186
|
+
vx = float(velocity["x"])
|
|
187
|
+
vy = float(velocity["y"])
|
|
188
|
+
vz = float(velocity["z"])
|
|
189
|
+
|
|
190
|
+
norm = math.sqrt(vx ** 2 + vy ** 2 + vz ** 2)
|
|
191
|
+
self.velocities.append(norm)
|
|
192
|
+
|
|
193
|
+
# info from auxiliary files
|
|
194
|
+
self.read_aux_info(aux_info)
|
|
195
|
+
|
|
196
|
+
# annotation info that does not change between bursts/blocks: read only once
|
|
197
|
+
self.antenna_patterns = read_antenna_patterns(self.metadata_dict, self.swath_id)
|
|
198
|
+
self.pg_correction_factors, self.pg_az_t = read_pg_correction_list(self.annotation, self.swath_id)
|
|
199
|
+
self.orbit = build_orbit(self.metadata_dict)
|
|
200
|
+
|
|
201
|
+
# other info
|
|
202
|
+
self.get_noise_power_correction(noise_power_from_l0)
|
|
203
|
+
self.range_over_sampling_factor = self.sampling_freq_afterdecimation() / self.rg_bandwidth()
|
|
204
|
+
self.rel_burst = None # only for TOPSAR SLC
|
|
205
|
+
self.range_t = None
|
|
206
|
+
self.range_distances = None
|
|
207
|
+
self.range_spread_loss = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_azimuth_processing(self, swath_name):
|
|
211
|
+
"""
|
|
212
|
+
Return the annotated azimuth processing parameters of a swath.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
swath_name : str
|
|
217
|
+
Swath identifier.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
dict
|
|
222
|
+
`azimuthProcessing` section of the swath processing parameters.
|
|
223
|
+
|
|
224
|
+
Raises
|
|
225
|
+
------
|
|
226
|
+
KeyError
|
|
227
|
+
If the swath is not found among the swath processing parameters.
|
|
228
|
+
"""
|
|
229
|
+
for sp in self.swath_params:
|
|
230
|
+
if sp["swath"] == swath_name:
|
|
231
|
+
return sp["azimuthProcessing"]
|
|
232
|
+
raise KeyError(f"Swath {swath_name} not found")
|
|
233
|
+
|
|
234
|
+
def get_range_processing(self, swath_name):
|
|
235
|
+
"""
|
|
236
|
+
Return the annotated range processing parameters of a swath.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
swath_name : str
|
|
241
|
+
Swath identifier.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
dict
|
|
246
|
+
`rangeProcessing` section of the swath processing parameters.
|
|
247
|
+
|
|
248
|
+
Raises
|
|
249
|
+
------
|
|
250
|
+
KeyError
|
|
251
|
+
If the swath is not found among the swath processing parameters.
|
|
252
|
+
"""
|
|
253
|
+
for sp in self.swath_params:
|
|
254
|
+
if sp["swath"] == swath_name:
|
|
255
|
+
return sp["rangeProcessing"]
|
|
256
|
+
raise KeyError(f"Swath {swath_name} not found")
|
|
257
|
+
|
|
258
|
+
def read_aux_info(self, aux_info: AuxiliaryFiles):
|
|
259
|
+
"""
|
|
260
|
+
Read the subswath and polarization parameters from the auxiliary files and store them as attributes.
|
|
261
|
+
|
|
262
|
+
Reads the processing gain and window coefficients from AUX_PP1; the azimuth antenna pattern (gains in dB
|
|
263
|
+
and angle increment in degrees), absolute calibration constant and noise calibration factor from AUX_CAL;
|
|
264
|
+
the PG reference, expected noise value and azimuth steering rate [rad/s] from AUX_INS.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
aux_info : AuxiliaryFiles
|
|
269
|
+
Auxiliary files used for processing.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
# from PP1
|
|
273
|
+
self.aux_pp1_calib = aux_info.get_gain_from_pp1(self.product_id, self.swath_id, self.polarization)
|
|
274
|
+
self.alpha_hamming, self.slc_alpha_hamming = aux_info.get_alpha_from_pp1(self.product_id, self.swath_id)
|
|
275
|
+
|
|
276
|
+
# from CAL
|
|
277
|
+
self.aap_gain = aux_info.get_aap_gain(self.swath_id, self.polarization)
|
|
278
|
+
self.aap_gain_count = aux_info.get_aap_gain_count(self.swath_id, self.polarization)
|
|
279
|
+
self.azi_angle_incr = aux_info.get_azi_angle_incr(self.swath_id, self.polarization)
|
|
280
|
+
self.absolute_calibration_constant = aux_info.get_abs_cal(self.swath_id, self.polarization)
|
|
281
|
+
self.noise_calibration_factor = aux_info.get_noise_cal(self.swath_id, self.polarization)
|
|
282
|
+
|
|
283
|
+
# from INS
|
|
284
|
+
self.pg_reference = aux_info.get_pg_reference(self.swath_id, self.polarization)
|
|
285
|
+
self.expected_noise_value = aux_info.get_expected_noise_value(self.swath_id, self.polarization)
|
|
286
|
+
self.azimuth_steering_rate = aux_info.get_azimuth_steering_rate(self.swath_id)
|
|
287
|
+
|
|
288
|
+
denoise_logger.debug(f"Absolute calibration constant: {self.absolute_calibration_constant}")
|
|
289
|
+
|
|
290
|
+
def rg_dim(self):
|
|
291
|
+
"""
|
|
292
|
+
Return the number of range samples of the image.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
int
|
|
297
|
+
Annotated `numberOfSamples`.
|
|
298
|
+
"""
|
|
299
|
+
return int(self.metadata_dict["product"]["imageAnnotation"]["imageInformation"]["numberOfSamples"])
|
|
300
|
+
|
|
301
|
+
def rg_bandwidth(self):
|
|
302
|
+
"""
|
|
303
|
+
Return the range total bandwidth of the subswath.
|
|
304
|
+
|
|
305
|
+
Returns
|
|
306
|
+
-------
|
|
307
|
+
float
|
|
308
|
+
Annotated range `totalBandwidth` [Hz].
|
|
309
|
+
|
|
310
|
+
Raises
|
|
311
|
+
------
|
|
312
|
+
ValueError
|
|
313
|
+
If the range processing parameters of the subswath are not found in the annotation.
|
|
314
|
+
"""
|
|
315
|
+
data = self.annotation.findall(
|
|
316
|
+
"imageAnnotation/processingInformation/swathProcParamsList/swathProcParams/swath"
|
|
317
|
+
)
|
|
318
|
+
for ind, element in enumerate(data):
|
|
319
|
+
if element.text == self.swath_id:
|
|
320
|
+
rg_bandwidth = float(
|
|
321
|
+
self.annotation.find(
|
|
322
|
+
"imageAnnotation/processingInformation/swathProcParamsList/swathProcParams[{}]/rangeProcessing/totalBandwidth".format(
|
|
323
|
+
ind + 1
|
|
324
|
+
)
|
|
325
|
+
).text
|
|
326
|
+
)
|
|
327
|
+
return rg_bandwidth
|
|
328
|
+
|
|
329
|
+
raise ValueError(f"Range processing parameters for swath {self.swath_id} not found in annotation")
|
|
330
|
+
|
|
331
|
+
def sampling_freq_afterdecimation(self):
|
|
332
|
+
"""
|
|
333
|
+
Return the range sampling frequency after decimation of the subswath.
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
float
|
|
338
|
+
Annotated `samplingFrequencyAfterDecimation` [Hz].
|
|
339
|
+
|
|
340
|
+
Raises
|
|
341
|
+
------
|
|
342
|
+
ValueError
|
|
343
|
+
If the downlink information of the subswath is not found in the annotation.
|
|
344
|
+
"""
|
|
345
|
+
data = self.annotation.findall(
|
|
346
|
+
"generalAnnotation/downlinkInformationList/downlinkInformation/swath"
|
|
347
|
+
)
|
|
348
|
+
for ind, element in enumerate(data):
|
|
349
|
+
if element.text == self.swath_id:
|
|
350
|
+
sampling_freq_afterdecimation = float(
|
|
351
|
+
self.annotation.find(
|
|
352
|
+
"generalAnnotation/downlinkInformationList/downlinkInformation[{}]/downlinkValues/rangeDecimation/samplingFrequencyAfterDecimation".format(
|
|
353
|
+
ind + 1
|
|
354
|
+
)
|
|
355
|
+
).text
|
|
356
|
+
)
|
|
357
|
+
return sampling_freq_afterdecimation
|
|
358
|
+
|
|
359
|
+
raise ValueError(f"Downlink information for swath {self.swath_id} not found in annotation")
|
|
360
|
+
|
|
361
|
+
def pg_correction_computation(self, azimuth_T):
|
|
362
|
+
"""
|
|
363
|
+
Interpolate the annotated PG correction factors at a given azimuth time.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
azimuth_T : PreciseDateTime
|
|
368
|
+
Azimuth time of interpolation.
|
|
369
|
+
|
|
370
|
+
Returns
|
|
371
|
+
-------
|
|
372
|
+
float
|
|
373
|
+
PG product amplitude linearly interpolated at `azimuth_T`.
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
pg_az_t = self.pg_az_t
|
|
377
|
+
|
|
378
|
+
pg_corr_az_central_t = np.interp(azimuth_T - pg_az_t[0], (pg_az_t - pg_az_t[0]).astype(float), self.pg_correction_factors)
|
|
379
|
+
|
|
380
|
+
return pg_corr_az_central_t
|
|
381
|
+
|
|
382
|
+
def get_noise_power_correction(self, noise_power_from_l0):
|
|
383
|
+
"""
|
|
384
|
+
Load the noise power values of the subswath and discard the outlier noise pulses.
|
|
385
|
+
|
|
386
|
+
The noise power is taken from the L0N product if provided, otherwise from the noise list of the product
|
|
387
|
+
annotation. Pulses outside the tolerance interval around the AUX_INS expected noise value are discarded
|
|
388
|
+
(a warning is logged for each one); if all of them are discarded, the expected noise value is used at all
|
|
389
|
+
the original azimuth times. Results are stored in `noise_az_t`, `noise_power_correction_factors` and
|
|
390
|
+
`outlier_detected`.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
noise_power_from_l0 : dict or None
|
|
395
|
+
Noise power extracted from the L0N product, mapping ``(swath, polarization)`` to
|
|
396
|
+
``[azimuth_times, noise_powers]``, or None to use the annotated noise list.
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
if noise_power_from_l0 is None:
|
|
400
|
+
noise_power_correction_factors, noise_az_t = read_noise_power_correction_from_annotation(self.metadata_dict, self.swath_id)
|
|
401
|
+
else:
|
|
402
|
+
noise_az_t = noise_power_from_l0.get((self.swath_id,self.polarization))[0]
|
|
403
|
+
noise_power_correction_factors = noise_power_from_l0.get((self.swath_id,self.polarization))[1]
|
|
404
|
+
|
|
405
|
+
# discard noise pulses
|
|
406
|
+
self.noise_az_t, self.noise_power_correction_factors, self.outlier_detected = remove_outliers(noise_az_t, noise_power_correction_factors, self.expected_noise_value)
|
|
407
|
+
|
|
408
|
+
for ind in range(len(self.outlier_detected)):
|
|
409
|
+
formatted = self.outlier_detected[ind][1].isoformat(timespec="microseconds").rstrip("Z")
|
|
410
|
+
denoise_logger.warning(f"Noise pulse at azimuth time {formatted} discarded")
|
|
411
|
+
|
|
412
|
+
# all noise pulses discarded: fall back to the nominal expected noise from AUX_INS
|
|
413
|
+
if self.noise_power_correction_factors.size == 0 and len(noise_az_t) > 0:
|
|
414
|
+
denoise_logger.warning(f"All noise pulses discarded for {self.swath_id} {self.polarization}: using nominal noise value from AUX_INS")
|
|
415
|
+
self.noise_az_t = np.array(noise_az_t)
|
|
416
|
+
self.noise_power_correction_factors = np.full(self.noise_az_t.size, self.expected_noise_value)
|
|
417
|
+
|
|
418
|
+
def noise_power_computation(self, azimuth_T):
|
|
419
|
+
"""
|
|
420
|
+
Interpolate the noise power at a given azimuth time.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
azimuth_T : PreciseDateTime
|
|
425
|
+
Azimuth time of interpolation.
|
|
426
|
+
|
|
427
|
+
Returns
|
|
428
|
+
-------
|
|
429
|
+
float
|
|
430
|
+
Noise power $P_n$ linearly interpolated at `azimuth_T` (held constant outside the noise pulse
|
|
431
|
+
time span).
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
# convert reference times into seconds relative to the first noise azimuth time
|
|
435
|
+
azimuth_T_sec = (azimuth_T - self.noise_az_t[0])
|
|
436
|
+
noise_az_t_sec = np.array([(t - self.noise_az_t[0]) for t in self.noise_az_t])
|
|
437
|
+
|
|
438
|
+
# perform interpolation in float domain
|
|
439
|
+
noise_power_correction_az_central_t = np.interp(
|
|
440
|
+
azimuth_T_sec,
|
|
441
|
+
noise_az_t_sec,
|
|
442
|
+
self.noise_power_correction_factors,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
denoise_logger.debug(f"Noise power correction: {noise_power_correction_az_central_t:.4}")
|
|
446
|
+
|
|
447
|
+
return noise_power_correction_az_central_t
|
|
448
|
+
|
|
449
|
+
def elevation_pattern(self, azimuth_T):
|
|
450
|
+
"""
|
|
451
|
+
Return the elevation antenna pattern gain on the current slant range axis.
|
|
452
|
+
|
|
453
|
+
Uses the antenna pattern record closest in azimuth time to `azimuth_T` and interpolates it on `range_t`.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
azimuth_T : PreciseDateTime
|
|
458
|
+
Azimuth time used to select the antenna pattern record.
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
numpy.ndarray
|
|
463
|
+
Elevation antenna pattern gain (modulus of the annotated complex pattern, linear) for each sample
|
|
464
|
+
of `range_t`.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
# elevation pattern used to compensate
|
|
468
|
+
idx = closest_antenna_pattern_index(self.antenna_patterns, azimuth_T)
|
|
469
|
+
|
|
470
|
+
final_elevation_pattern_gain = np.interp(
|
|
471
|
+
self.range_t,
|
|
472
|
+
self.antenna_patterns["slant_range_t"][idx],
|
|
473
|
+
self.antenna_patterns["elevation_pattern_gain"][idx]
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return final_elevation_pattern_gain
|
|
477
|
+
|
|
478
|
+
def incidence_angles(self, azimuth_T):
|
|
479
|
+
"""
|
|
480
|
+
Return the incidence angles on the current slant range axis.
|
|
481
|
+
|
|
482
|
+
Uses the antenna pattern record closest in azimuth time to `azimuth_T` and interpolates it on `range_t`.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
azimuth_T : PreciseDateTime
|
|
487
|
+
Azimuth time used to select the antenna pattern record.
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
numpy.ndarray
|
|
492
|
+
Incidence angles [deg] for each sample of `range_t`.
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
# incidence angles used to compensate
|
|
496
|
+
idx = closest_antenna_pattern_index(self.antenna_patterns, azimuth_T)
|
|
497
|
+
|
|
498
|
+
final_incidence_angles = np.interp(
|
|
499
|
+
self.range_t,
|
|
500
|
+
self.antenna_patterns["slant_range_t"][idx],
|
|
501
|
+
self.antenna_patterns["incidence_angles"][idx]
|
|
502
|
+
)
|
|
503
|
+
return final_incidence_angles
|
|
504
|
+
|
|
505
|
+
def compute_k_proc(self):
|
|
506
|
+
"""
|
|
507
|
+
Compute the processing gain $k_{proc}$.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
float
|
|
512
|
+
Squared AUX_PP1 gain divided by the range oversampling factor.
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
k_proc = ( self.aux_pp1_calib / np.sqrt(self.range_over_sampling_factor) ) ** 2
|
|
516
|
+
|
|
517
|
+
return k_proc
|
|
518
|
+
|
|
519
|
+
def compute_G_tot(self, azimuth_T):
|
|
520
|
+
"""
|
|
521
|
+
Compute the total gain applied to the noise power along the current slant range axis.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
azimuth_T : PreciseDateTime
|
|
526
|
+
Azimuth time at which the PG correction and the elevation antenna pattern are evaluated.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
numpy.ndarray
|
|
531
|
+
Total gain for each sample of `range_t`.
|
|
532
|
+
|
|
533
|
+
Notes
|
|
534
|
+
-----
|
|
535
|
+
The total gain is
|
|
536
|
+
|
|
537
|
+
$$
|
|
538
|
+
G_{tot} = \\frac{(G_{pg} \\cdot PG_{ref})^2 \\cdot k_{noise} \\cdot k_{proc}}{G_{eap} \\cdot G_{rsl}}
|
|
539
|
+
$$
|
|
540
|
+
|
|
541
|
+
where $G_{pg}$ is the interpolated PG correction, $PG_{ref}$ the AUX_INS PG reference,
|
|
542
|
+
$k_{noise}$ the AUX_CAL noise calibration factor, $G_{eap}$ the elevation antenna pattern gain
|
|
543
|
+
and $G_{rsl}$ the range spreading loss correction.
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
pg = self.pg_correction_computation(azimuth_T) * self.pg_reference
|
|
547
|
+
k_N = self.noise_calibration_factor
|
|
548
|
+
k_proc = self.compute_k_proc()
|
|
549
|
+
elev_pattern = self.elevation_pattern(azimuth_T)
|
|
550
|
+
rsl = self.range_spread_loss
|
|
551
|
+
|
|
552
|
+
G_tot = pg**2 * k_N * k_proc / (elev_pattern * rsl)
|
|
553
|
+
|
|
554
|
+
return G_tot
|
|
555
|
+
|
|
556
|
+
def compute_simulated_noise(self, azimuth_T) -> NDArray[np.float64]:
|
|
557
|
+
"""
|
|
558
|
+
Compute the simulated noise range vector at a given azimuth time, in slant range.
|
|
559
|
+
|
|
560
|
+
Also reconstructs the noise normalization factor (see `reconstruct_noise_norm_factor`), so
|
|
561
|
+
`set_range_values` and `get_shrink_factor` must have been called beforehand.
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
azimuth_T : PreciseDateTime
|
|
566
|
+
Azimuth time of the noise vector (burst center time for SLC, antenna pattern record time for GRD).
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
simulated_noise : numpy.ndarray
|
|
571
|
+
Simulated noise $\\sin(\\theta_i) \\cdot P_n \\cdot G_{tot} \\cdot k_{norm}$ (linear power) for each
|
|
572
|
+
sample of `range_t`.
|
|
573
|
+
sin_incidence : numpy.ndarray
|
|
574
|
+
Sine of the incidence angle $\\theta_i$ for each sample of `range_t`.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
inc_angles = np.deg2rad(self.incidence_angles(azimuth_T))
|
|
578
|
+
noise_pwr = self.noise_power_computation(azimuth_T)
|
|
579
|
+
G_tot = self.compute_G_tot(azimuth_T)
|
|
580
|
+
|
|
581
|
+
self.reconstruct_noise_norm_factor()
|
|
582
|
+
|
|
583
|
+
simulated_noise = np.sin(inc_angles) * noise_pwr * G_tot * self.noise_norm_factor
|
|
584
|
+
|
|
585
|
+
return np.array(simulated_noise, dtype=np.float64), np.sin(inc_angles)
|
|
586
|
+
|
|
587
|
+
def get_shrink_factor(self):
|
|
588
|
+
"""
|
|
589
|
+
Compute the TopSAR azimuth shrink factor of the current burst and store it in `shrink_factor`.
|
|
590
|
+
|
|
591
|
+
The shrink factor is $1 - k_{DC} / k_{FM}$, where $k_{FM}$ is the azimuth FM rate of the burst
|
|
592
|
+
(`rel_burst`) and $k_{DC}$ the Doppler centroid rate derived from the AUX_INS azimuth steering rate,
|
|
593
|
+
both evaluated at mid-range as in the IPF. It requires `range_t` and `rel_burst` to be set.
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
range_filter = int(self.rel_burst is not None)
|
|
597
|
+
range_times = self.range_t[::range_filter]
|
|
598
|
+
|
|
599
|
+
# deramping matrix
|
|
600
|
+
FM_rate, az_mid_time = extract_FR_info(self.metadata_dict, range_times, self.rel_burst, self.is_grd, self.swath_id)
|
|
601
|
+
FM_rate_mid = FM_rate[range_times.size // 2]
|
|
602
|
+
|
|
603
|
+
AR_vec = extract_AR_info(self.metadata_dict, az_mid_time, range_times, self.azimuth_steering_rate, orbit=self.orbit)
|
|
604
|
+
|
|
605
|
+
# give back FM rate and DC rate evaluated at mid-range like IPF
|
|
606
|
+
doppler_centroid_rate = AR_vec[range_times.size // 2]
|
|
607
|
+
|
|
608
|
+
self.shrink_factor = 1 - doppler_centroid_rate / FM_rate_mid
|
|
609
|
+
|
|
610
|
+
def set_range_values(self, range_t):
|
|
611
|
+
"""
|
|
612
|
+
Set the slant range axis of the noise vector and derive the range-dependent quantities.
|
|
613
|
+
|
|
614
|
+
Stores the slant range times in `range_t`, the slant range distances in `range_distances` and the range
|
|
615
|
+
spreading loss correction $(R_{ref} / R)^4$ in `range_spread_loss`.
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
range_t : numpy.ndarray
|
|
620
|
+
Slant range times of the noise vector samples [s].
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
self.range_t = range_t # [s]
|
|
624
|
+
self.range_distances = self.range_t * speed_of_light / 2 # [m]
|
|
625
|
+
self.range_spread_loss = (R_REF / self.range_distances) ** 4
|
|
626
|
+
|
|
627
|
+
def reconstruct_noise_norm_factor(self):
|
|
628
|
+
"""
|
|
629
|
+
Reconstruct the IPF noise normalization factor and store it in `noise_norm_factor`.
|
|
630
|
+
|
|
631
|
+
Reverse-engineered from the IPF. The azimuth contribution is obtained by interpolating the AUX_CAL azimuth
|
|
632
|
+
antenna pattern on the azimuth FFT grid (angles converted to frequencies using the `shrink_factor`),
|
|
633
|
+
extracting the overlapping looks with a Hamming window, and averaging over the looks the ratio between the
|
|
634
|
+
noise processing gain and the window normalization factor. The range contribution accounts for the rounding
|
|
635
|
+
of the range processing bandwidth to an integer number of samples (flat spectrum). The stored value is the
|
|
636
|
+
product of the two.
|
|
637
|
+
|
|
638
|
+
Raises
|
|
639
|
+
------
|
|
640
|
+
ValueError
|
|
641
|
+
If the azimuth or range processing bandwidth corresponds to zero samples, or if a window
|
|
642
|
+
normalization factor is zero.
|
|
643
|
+
|
|
644
|
+
Notes
|
|
645
|
+
-----
|
|
646
|
+
$$
|
|
647
|
+
k_{norm} = k_{norm,az} \\cdot k_{norm,rg}, \\qquad
|
|
648
|
+
k_{norm,az} = \\frac{1}{L} \\sum_{l=1}^{L} \\frac{G_l}{W_l}, \\qquad
|
|
649
|
+
k_{norm,rg} = \\frac{N_{bw,rg}}{\\mathrm{round}(N_{bw,rg})}
|
|
650
|
+
$$
|
|
651
|
+
|
|
652
|
+
See the "Noise normalization factor" page of the algorithm documentation for details.
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
# 1. Constants and basic frequency parameters
|
|
656
|
+
|
|
657
|
+
LAMBDA = speed_of_light / self.radar_frequency
|
|
658
|
+
|
|
659
|
+
fft_length = find_fft_size(self.lines_per_burst)
|
|
660
|
+
sampling_freq_az = 1.0 / self.input_azi_spacing
|
|
661
|
+
delta_freq = sampling_freq_az / fft_length
|
|
662
|
+
|
|
663
|
+
bw_in_samples = int(np.round(self.processing_bandwidth / delta_freq))
|
|
664
|
+
if bw_in_samples == 0:
|
|
665
|
+
raise ValueError("Zero samples in bandwith")
|
|
666
|
+
|
|
667
|
+
# window length is given by the look bandwidth, as in IPF
|
|
668
|
+
window_len = int(np.round(self.look_bandwidth / delta_freq))
|
|
669
|
+
window = hamming(window_len, alpha=self.azi_window_coefficient)
|
|
670
|
+
|
|
671
|
+
look_overlap = get_look_overlap_samples(
|
|
672
|
+
window_len=window_len,
|
|
673
|
+
bw_in_samples=bw_in_samples,
|
|
674
|
+
number_of_looks=self.number_of_looks
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
look_start_indices = get_start_indices(
|
|
678
|
+
look_overlap=look_overlap,
|
|
679
|
+
window_len=window_len,
|
|
680
|
+
in_line_len=fft_length,
|
|
681
|
+
number_of_looks=self.number_of_looks
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# 2. Angle computation
|
|
685
|
+
|
|
686
|
+
angle_incr_rad = LAMBDA / (2 * np.mean(self.velocities))
|
|
687
|
+
angle_incr_rad *= delta_freq
|
|
688
|
+
angle_incr_rad *= self.shrink_factor
|
|
689
|
+
|
|
690
|
+
denoise_logger.debug(f"Shrink factor: {self.shrink_factor:.2f}")
|
|
691
|
+
|
|
692
|
+
angle_incr_deg = np.rad2deg(angle_incr_rad)
|
|
693
|
+
|
|
694
|
+
output_start_angle = -fft_length / 2 * angle_incr_deg
|
|
695
|
+
input_start_angle = -self.azi_angle_incr * (self.aap_gain_count - 1) / 2
|
|
696
|
+
|
|
697
|
+
output_az_axis = np.arange(fft_length) * angle_incr_deg + output_start_angle
|
|
698
|
+
input_az_axis = (
|
|
699
|
+
np.arange(self.aap_gain_count) * self.azi_angle_incr + input_start_angle
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
# 3. Antenna pattern interpolation and correction
|
|
703
|
+
|
|
704
|
+
aap_data = np.interp(x=output_az_axis, xp=input_az_axis, fp=self.aap_gain)
|
|
705
|
+
aap_data = np.fft.fftshift(aap_data)
|
|
706
|
+
|
|
707
|
+
data_spectra = db_to_linear(aap_data, is_amplitude=True)
|
|
708
|
+
|
|
709
|
+
# 4. Window normalization
|
|
710
|
+
|
|
711
|
+
window_norm_factor = calculate_window_norm_factor(
|
|
712
|
+
data_spectra=data_spectra,
|
|
713
|
+
in_line_len=fft_length,
|
|
714
|
+
window=window,
|
|
715
|
+
bw_in_samples=bw_in_samples,
|
|
716
|
+
look_start_indices=look_start_indices,
|
|
717
|
+
correct_spectra=self.apply_azi_ant_pattern
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
sum_window_sq_noise = update_window_noise_norm(
|
|
721
|
+
data_spectra=data_spectra,
|
|
722
|
+
window=window,
|
|
723
|
+
look_start_indices=look_start_indices,
|
|
724
|
+
in_line_len=fft_length,
|
|
725
|
+
correct_spectra=self.apply_azi_ant_pattern
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# 5. Noise normalization factor computation
|
|
729
|
+
|
|
730
|
+
if np.any(window_norm_factor == 0.0):
|
|
731
|
+
raise ValueError("Zero window normalization factor")
|
|
732
|
+
|
|
733
|
+
noise_proc_gain = sum_window_sq_noise / bw_in_samples
|
|
734
|
+
noise_norm_factor_all_looks = noise_proc_gain / window_norm_factor
|
|
735
|
+
|
|
736
|
+
noise_azi_norm_factor = np.mean(noise_norm_factor_all_looks)
|
|
737
|
+
|
|
738
|
+
# 6. Range noise normalization factor (flat spectrum in range, as in IPF):
|
|
739
|
+
# window norm uses the exact bandwidth in samples, noise gain the rounded one
|
|
740
|
+
|
|
741
|
+
rg_fft_length = find_fft_size(self.samples_per_burst if self.samples_per_burst > 0 else self.rg_dim())
|
|
742
|
+
rg_delta_freq = self.sampling_freq_afterdecimation() / rg_fft_length
|
|
743
|
+
rg_bw_samples = self.rg_processing_bandwidth / rg_delta_freq
|
|
744
|
+
if int(np.round(rg_bw_samples)) == 0:
|
|
745
|
+
raise ValueError("Zero samples in range bandwith")
|
|
746
|
+
|
|
747
|
+
noise_rng_norm_factor = rg_bw_samples / int(np.round(rg_bw_samples))
|
|
748
|
+
|
|
749
|
+
denoise_logger.debug(f"Noise azimuth norm factor: {noise_azi_norm_factor}, range norm factor: {noise_rng_norm_factor}")
|
|
750
|
+
|
|
751
|
+
self.noise_norm_factor = noise_azi_norm_factor * noise_rng_norm_factor
|
|
752
|
+
|
|
753
|
+
denoise_logger.debug(f"Noise norm factor: {self.noise_norm_factor}")
|