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/logger.py ADDED
@@ -0,0 +1,81 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Logging configuration module for the denoising processor.
6
+
7
+ Sets up a named logger with colorized console output and plain-text file output, using a structured log format
8
+ that includes timestamp, hostname, processor name, version, and process ID. Exposes a module-level
9
+ ``denoise_logger`` instance for use across the package.
10
+ """
11
+
12
+ import logging
13
+ import colorlog
14
+ import socket
15
+
16
+ from s1_denoise import __version__, PROCESSOR_NAME
17
+
18
+ def setup_logger():
19
+ """
20
+ Set up the ``denoise`` logger with colorized console output and file output.
21
+
22
+ Handlers are attached only if the logger has none yet, so repeated calls do not duplicate output. Log levels
23
+ are renamed to short tags (``[D]``, ``[I]``, ``[W]``, ``[E]``, ``[P]``), records are written both to the console
24
+ (colorized) and to ``denoise.log`` in the current working directory, and the level is set to ``INFO``.
25
+
26
+ Returns
27
+ -------
28
+ logging.Logger
29
+ The configured ``denoise`` logger.
30
+ """
31
+
32
+ # Check if the logger has already handlers attached
33
+ logger = logging.getLogger("denoise")
34
+
35
+ # Only set up logging if no handlers are already attached
36
+ if not logger.hasHandlers():
37
+
38
+ logging.addLevelName(logging.NOTSET, "")
39
+ logging.addLevelName(logging.DEBUG, "[D]")
40
+ logging.addLevelName(logging.INFO, "[I]")
41
+ logging.addLevelName(logging.WARNING, "[W]")
42
+ logging.addLevelName(logging.ERROR, "[E]")
43
+ logging.addLevelName(logging.CRITICAL, "[P]")
44
+
45
+ log_colors = {
46
+ '': 'green',
47
+ '[D]': 'cyan',
48
+ # '[I]': 'white',
49
+ '[W]': 'yellow',
50
+ '[E]': 'red',
51
+ '[P]': 'bold_red',
52
+ }
53
+
54
+ HOSTNAME=socket.gethostname()
55
+
56
+ LOG_STRING = f'%(asctime)s.%(msecs)03d000 {HOSTNAME} {PROCESSOR_NAME} {__version__} [%(process)d] %(levelname)s %(message)s'
57
+ COLORED_LOG_STRING = "%(log_color)s" + LOG_STRING
58
+ DATE_FMT = "%Y-%m-%dT%H:%M:%S"
59
+
60
+ # Create a colored formatter
61
+ formatter = colorlog.ColoredFormatter(COLORED_LOG_STRING, log_colors=log_colors, datefmt=DATE_FMT)
62
+
63
+ # Create console handler for colored logs
64
+ console_handler = logging.StreamHandler()
65
+ console_handler.setFormatter(formatter)
66
+
67
+ # Create file handler to log to "denoise.log"
68
+ file_handler = logging.FileHandler("denoise.log")
69
+ file_handler.setFormatter(logging.Formatter(LOG_STRING, datefmt=DATE_FMT))
70
+
71
+ # Create logger and set level
72
+ logger = logging.getLogger("denoise")
73
+
74
+ logger.addHandler(console_handler)
75
+ logger.addHandler(file_handler)
76
+
77
+ logger.setLevel(logging.INFO)
78
+
79
+ return logger
80
+
81
+ denoise_logger = setup_logger()
@@ -0,0 +1,291 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Utility module providing signal processing helpers for noise profile generation.
6
+
7
+ Includes functions for FFT size selection, Hamming window computation, look
8
+ start index and overlap calculation, and window normalisation factor estimation.
9
+
10
+ IPF noise normalization factor
11
+ ------------------------------
12
+ During L1 post-processing (LBP), each look is extracted with a Hamming window
13
+ and, in azimuth, possibly corrected for the azimuth antenna pattern (AAP). The
14
+ signal is then scaled by the window normalization factor so that its power is
15
+ preserved:
16
+
17
+ windowNorm = sum(w^2) / sum(AAP^2 over the processing bandwidth)
18
+
19
+ Thermal noise has a flat spectrum, so this normalization changes its power
20
+ differently. Since the AAP correction (division by AAP) also boosts the noise
21
+ at the spectrum edges, its processing gain is:
22
+
23
+ noiseProcGain = sum(w^2 / AAP^2) / bwInSamples
24
+
25
+ The noise normalization factor is the ratio between the two, averaged over the
26
+ looks:
27
+
28
+ noiseNormFactor = mean_looks(noiseProcGain / windowNorm)
29
+
30
+ It is computed in azimuth and in range (flat spectrum, so the range factor is
31
+ ~1) and the final value is their product. The IPF multiplies the noise vectors
32
+ by this factor when noiseNormalizationFlag is set, and annotates its average
33
+ over the blocks of each swath in noiseNormalizationFactorList (1.0 if the flag
34
+ is not set). If the AAP correction is not applied, AAP is not divided out:
35
+ sum(w^2) is used in noiseProcGain and sum((AAP * w)^2) per look in windowNorm.
36
+ """
37
+
38
+ import numpy as np
39
+ import math
40
+
41
+ def find_fft_size(desired_fft_size):
42
+ """
43
+ Return the smallest valid IPF FFT length not lower than the desired size.
44
+
45
+ Parameters
46
+ ----------
47
+ desired_fft_size : int
48
+ Minimum required FFT length [samples].
49
+
50
+ Returns
51
+ -------
52
+ int
53
+ Smallest FFT length in the list of lengths supported by the IPF that is >= `desired_fft_size`.
54
+
55
+ Raises
56
+ ------
57
+ ValueError
58
+ If `desired_fft_size` exceeds the largest supported FFT length.
59
+ """
60
+
61
+ FFT_VALID_LENGTHS = [ 240, 256, 288, 320, 384, 480, 512, 576, 640, 720, 768, 960, 1024, 1152, 1300, 1400, 1584, 1800, 1950, 2080, 2464, 2560, 2880, 3072, 3564, 3750, 3822, 4096, 4400, 5000, 5760, 6144, 7840, 8100, 8400, 8640, 8736, 8820, 9072, 9216, 11520, 12288, 15360, 15552, 16000, 16384, 17280, 18000, 18480, 19008, 19440, 19500, 20000, 20580, 22400, 23760, 24576, 25920, 27648, 28800, 30720, 31104, 32000, 32768, 34560, 36000, 36864, 38400, 40000, 40960, 41472, 43200, 46080, 48000, 49152, 51200, 51840, 54000, 55296, 57600, 60000, 61440, 62208, 64000, 64800, 65536, 69120, 72000, 73728, 76800, 77760, 80000, 81920, 82944, 86400, 92160, 93312, 96000, 98304 ]
62
+
63
+ for size in FFT_VALID_LENGTHS:
64
+
65
+ if desired_fft_size <= size:
66
+ return size
67
+
68
+ raise ValueError(f"No valid FFT size found for the desired FFT size {desired_fft_size}.")
69
+
70
+ def update_window_noise_norm(data_spectra, window, look_start_indices, in_line_len, correct_spectra=True):
71
+ """
72
+ Compute the per-look sum of the squared window divided by the squared spectrum (noise processing gain numerator).
73
+
74
+ For each look, the window is aligned to the FFT grid at the look start index (wrapping around the grid) and
75
+ ``sum((window / data_spectra)**2)`` is computed. If the spectra are not corrected (or not given), the sum of the
76
+ squared window is used for each look, as in the IPF.
77
+
78
+ Parameters
79
+ ----------
80
+ data_spectra : numpy.ndarray or None
81
+ Azimuth antenna pattern on the FFT grid, in linear amplitude, with at least `in_line_len` samples.
82
+ window : numpy.ndarray
83
+ Look window (Hamming) of length not greater than `in_line_len`.
84
+ look_start_indices : array_like of int
85
+ Start index of each look on the FFT grid, lower than `in_line_len`.
86
+ in_line_len : int
87
+ FFT length [samples].
88
+ correct_spectra : bool, optional
89
+ Whether the azimuth antenna pattern correction is applied (division by the AAP). Default is True.
90
+
91
+ Returns
92
+ -------
93
+ numpy.ndarray
94
+ Sum of squared (window / AAP) for each look, or of squared window if the correction is not applied.
95
+
96
+ Raises
97
+ ------
98
+ ValueError
99
+ If a look start index is not lower than `in_line_len`, if the window is longer than `in_line_len`, or if
100
+ `data_spectra` is shorter than `in_line_len`.
101
+ """
102
+
103
+ window = np.asarray(window, dtype=np.float64)
104
+ look_start_indices = np.asarray(look_start_indices, dtype=np.int64)
105
+
106
+ window_len = window.size
107
+ window_sq = window ** 2
108
+
109
+ if data_spectra is None or not correct_spectra:
110
+ return np.full(look_start_indices.size, np.sum(window_sq))
111
+
112
+ data_spectra = np.asarray(data_spectra, dtype=np.float64)
113
+
114
+ if np.any(look_start_indices >= in_line_len):
115
+ raise ValueError(f"look_start_indices ({look_start_indices}) must be less than in_line_len ({in_line_len})")
116
+ if window_len > in_line_len:
117
+ raise ValueError(f"window length ({window_len}) must be <= in_line_len ({in_line_len})")
118
+ if data_spectra.size < in_line_len:
119
+ raise ValueError(f"data_spectra is shorter ({data_spectra.size}) than in_line_len ({in_line_len})")
120
+
121
+ base_idx = np.arange(window_len, dtype=np.int64)
122
+
123
+ idx = (look_start_indices[:, None] + base_idx) % in_line_len
124
+ segments = data_spectra[idx]
125
+
126
+ sum_window_sq_noise = np.sum(window_sq / (segments**2), axis=1)
127
+
128
+ return sum_window_sq_noise
129
+
130
+
131
+ def calculate_window_norm_factor(data_spectra, bw_in_samples, in_line_len, window, look_start_indices, correct_spectra=True):
132
+ """
133
+ Compute the window normalization factor of each look (IPF ``LBP_GenerateLook::calcNormFactor``).
134
+
135
+ The factor is the sum of the squared window divided by the sum of the squared spectrum over the processing
136
+ bandwidth, centered on the zero frequency. If the spectra are not corrected, the spectrum remaining at the end
137
+ of processing is included in the sum of the squared window, separately for each look.
138
+
139
+ Parameters
140
+ ----------
141
+ data_spectra : numpy.ndarray or None
142
+ Azimuth antenna pattern on the FFT grid, in linear amplitude. If None, a flat unit spectrum is assumed.
143
+ bw_in_samples : int
144
+ Processing bandwidth [samples].
145
+ in_line_len : int
146
+ FFT length [samples].
147
+ window : numpy.ndarray
148
+ Look window (Hamming).
149
+ look_start_indices : array_like of int
150
+ Start index of each look on the FFT grid.
151
+ correct_spectra : bool, optional
152
+ Whether the azimuth antenna pattern correction is applied (division by the AAP). Default is True.
153
+
154
+ Returns
155
+ -------
156
+ numpy.ndarray
157
+ Window normalization factor for each look.
158
+ """
159
+
160
+ window = np.asarray(window, dtype=np.float64)
161
+ look_start_indices = np.asarray(look_start_indices, dtype=np.int64)
162
+
163
+ if data_spectra is not None:
164
+ data_spectra = np.array(data_spectra, dtype=np.float32)
165
+
166
+ start_index = math.floor((-1.0 * bw_in_samples + 1) / 2)
167
+ start_index %= in_line_len
168
+
169
+ indices = (start_index + np.arange(bw_in_samples)) % in_line_len
170
+
171
+ selected = np.take(data_spectra, indices, mode="wrap")
172
+
173
+ sum_adjustment_sq = np.sum(selected ** 2)
174
+
175
+ else:
176
+ sum_adjustment_sq = bw_in_samples
177
+
178
+ if data_spectra is not None and not correct_spectra:
179
+ idx = (look_start_indices[:, None] + np.arange(window.size)) % in_line_len
180
+ sum_window_sq = np.sum((data_spectra[idx] * window) ** 2, axis=1)
181
+ else:
182
+ sum_window_sq = np.full(look_start_indices.size, np.sum(window ** 2))
183
+
184
+ window_norm_factor = sum_window_sq / sum_adjustment_sq
185
+
186
+ return window_norm_factor
187
+
188
+ def hamming(nsize, alpha):
189
+ """
190
+ Compute a generalized Hamming window as in the IPF.
191
+
192
+ The window is ``alpha + (1 - alpha) * cos(2 * pi * (n - centre) / (nsize - 1))`` for ``n = 1, ..., nsize``,
193
+ with ``centre = (nsize + 1) / 2``.
194
+
195
+ Parameters
196
+ ----------
197
+ nsize : int
198
+ Window length [samples].
199
+ alpha : float
200
+ Window coefficient. Values >= 1 give a flat (rectangular) window.
201
+
202
+ Returns
203
+ -------
204
+ numpy.ndarray or None
205
+ Window samples, or None if `alpha` is negative.
206
+ """
207
+
208
+ whamm = np.zeros(nsize, dtype=np.float64)
209
+
210
+ # alpha >= 1.0 -> flat window
211
+ if alpha >= 1.0:
212
+ whamm[:] = 1.0
213
+ return whamm
214
+
215
+ if alpha < 0.0:
216
+ return None
217
+
218
+ # Constants
219
+ TWO_PI = 2.0 * math.pi
220
+ centre = float(nsize + 1) / 2.0 # central index
221
+ fsize = float(nsize - 1)
222
+
223
+ # Hamming (sample indices from 1 to nsize)
224
+ arg = TWO_PI * (np.arange(1, nsize + 1, dtype=np.float64) - centre) / fsize
225
+ whamm = alpha + (1.0 - alpha) * np.cos(arg)
226
+
227
+ return whamm
228
+
229
+ def get_look_overlap_samples(window_len, bw_in_samples, number_of_looks = 1):
230
+ """
231
+ Compute the look overlap in frequency samples, as in the IPF (``LBP_GenerateLook::initBlock``).
232
+
233
+ Parameters
234
+ ----------
235
+ window_len : int
236
+ Look length [samples].
237
+ bw_in_samples : int
238
+ Processing bandwidth [samples].
239
+ number_of_looks : int, optional
240
+ Number of looks. Default is 1.
241
+
242
+ Returns
243
+ -------
244
+ int
245
+ Overlap between consecutive looks [samples], ``ceil((L * window_len - bw_in_samples) / (L - 1))`` for
246
+ ``L > 1`` looks, 0 for single-look processing.
247
+ """
248
+
249
+ if number_of_looks <= 1:
250
+ return 0
251
+
252
+ extra_samples = window_len * number_of_looks - bw_in_samples
253
+
254
+ return int(math.ceil(extra_samples / (number_of_looks - 1.0)))
255
+
256
+ def get_start_indices(look_overlap, window_len, in_line_len, number_of_looks = 1):
257
+ """
258
+ Compute the start index of each look on the FFT grid.
259
+
260
+ The looks are centered on the zero frequency and spaced by ``window_len - look_overlap`` samples; indices are
261
+ wrapped modulo `in_line_len`.
262
+
263
+ Parameters
264
+ ----------
265
+ look_overlap : int
266
+ Overlap between consecutive looks [samples].
267
+ window_len : int
268
+ Look length [samples].
269
+ in_line_len : int
270
+ FFT length [samples].
271
+ number_of_looks : int, optional
272
+ Number of looks. Default is 1.
273
+
274
+ Returns
275
+ -------
276
+ list of int
277
+ Start index of each look on the FFT grid.
278
+ """
279
+
280
+ extracted_freq_samples = window_len + (window_len - look_overlap) * (number_of_looks - 1)
281
+ start_offset = int(np.floor((1.0 - extracted_freq_samples) / 2))
282
+
283
+ look_start_indices = [0] * number_of_looks
284
+ look_end_indices = [0] * number_of_looks
285
+
286
+ for lookIdx in range(number_of_looks):
287
+ look_start_indices[lookIdx] = int((start_offset + (window_len - look_overlap) * lookIdx) % in_line_len)
288
+
289
+ look_end_indices[lookIdx] = int((look_start_indices[lookIdx] + window_len - 1) % in_line_len)
290
+
291
+ return look_start_indices
@@ -0,0 +1,292 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Core module orchestrating the generation of simulated thermal noise profiles for
6
+ Sentinel-1 SLC and GRD products.
7
+
8
+ Iterates over all polarisation/subswath channels of a SAFE product, applies
9
+ thermal noise modelling via the ThermalNoise class, and returns structured output
10
+ noise vectors. Optionally incorporates noise power correction derived from the
11
+ associated L0N file.
12
+ """
13
+
14
+ import sys
15
+ from pathlib import Path
16
+ import numpy as np
17
+
18
+ from perseo_core.timing import PreciseDateTime
19
+
20
+ # Local imports
21
+ from s1_denoise.logger import denoise_logger
22
+ from s1_denoise.s1safe import S1Safe
23
+ from s1_denoise.xml_reader import create_slant_range_times, read_metadata_from_safe_annotation
24
+ from s1_denoise.thermal_noise import ThermalNoise
25
+ from s1_denoise.auxiliary_files import AuxiliaryFiles
26
+ from s1_denoise.output import OutputNoiseVectors_SLC, OutputNoiseVectors_GRD
27
+ from s1_denoise.l0_noise_power_calculation import get_noise_pwr_from_l0n
28
+ from s1_denoise.utils import ensure_list, slant2ground
29
+
30
+ def print_exec_time(elapsed_seconds: float) -> None:
31
+ """
32
+ Log the execution time in a human-readable format.
33
+
34
+ The time is split into hours, minutes and seconds and logged at debug level.
35
+
36
+ Parameters
37
+ ----------
38
+ elapsed_seconds : float
39
+ Elapsed time [s].
40
+ """
41
+
42
+ hours = int(elapsed_seconds // 3600)
43
+ remaining = elapsed_seconds % 3600
44
+ minutes = int(remaining // 60)
45
+ seconds = remaining % 60
46
+
47
+ time_parts = []
48
+ if hours > 0:
49
+ time_parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
50
+ if minutes > 0:
51
+ time_parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
52
+ if seconds > 0 or not time_parts:
53
+ time_parts.append(f"{seconds:.3f} seconds")
54
+
55
+ denoise_logger.debug("Execution time: %s", ", ".join(time_parts))
56
+
57
+ def slc_processing(safe, aux_files, dn_value, noise_power_correction):
58
+ """
59
+ Generate the simulated noise range vectors of an SLC product.
60
+
61
+ For each polarization/subswath channel, one noise vector is computed per burst, at the burst center azimuth
62
+ time, on the slant range axis of the swath.
63
+
64
+ Parameters
65
+ ----------
66
+ safe : S1Safe
67
+ Input SLC SAFE product.
68
+ aux_files : AuxiliaryFiles
69
+ Auxiliary files used for processing.
70
+ dn_value : float
71
+ Digital number calibration value from AUX_PP1, used to scale the output noise values.
72
+ noise_power_correction : dict or None
73
+ Noise power extracted from the L0N product, mapping ``(swath, polarization)`` to
74
+ ``[azimuth_times, noise_powers]``, or None to use the noise power annotated in the product.
75
+
76
+ Returns
77
+ -------
78
+ list of OutputNoiseVectors_SLC
79
+ Output noise vectors, one element per channel.
80
+ """
81
+
82
+ simulated_noises = []
83
+
84
+ # loop on annotation files
85
+ for pol, subswath in safe._channels:
86
+
87
+ channel = safe.channel(pol, subswath)
88
+ denoise_logger.info(f"Processing bursts from polarization {pol}, subswath {subswath}")
89
+
90
+ metadata_dict = read_metadata_from_safe_annotation(channel.annotation)
91
+ ads_header = metadata_dict['product']['adsHeader']
92
+ lines_per_burst = int(metadata_dict['product']['swathTiming']['linesPerBurst'])
93
+ az_dt = float(metadata_dict['product']['imageAnnotation']['imageInformation']['azimuthTimeInterval'])
94
+
95
+ thermal_noise = ThermalNoise(metadata_dict=metadata_dict,
96
+ channel=channel,
97
+ aux_info=aux_files,
98
+ product_id=safe.path.stem[4:13],
99
+ noise_power_from_l0=noise_power_correction)
100
+
101
+ sim_noises = {}
102
+ geo_grid_info = metadata_dict['product']['geolocationGrid']
103
+
104
+ # slant range times are the same for all the bursts of the swath
105
+ range_t = create_slant_range_times(metadata_dict)
106
+ thermal_noise.set_range_values(range_t)
107
+
108
+ denoise_logger.info(f"Generating noise profiles for subswath {subswath} ({pol})")
109
+
110
+ for rel_burst in range(channel.num_bursts):
111
+
112
+ denoise_logger.debug(f"Generating noise profiles for burst {rel_burst+1} ({pol}, {subswath})")
113
+
114
+ az_t0 = PreciseDateTime.from_utc_string((channel.annotation.find('antennaPattern/antennaPatternList/antennaPattern[{}]/azimuthTime'.format(rel_burst+1)).text))
115
+
116
+ az_central_time = az_t0 + az_dt * lines_per_burst/2
117
+
118
+ # create simulated noise
119
+ thermal_noise.rel_burst = rel_burst
120
+ thermal_noise.get_shrink_factor()
121
+
122
+ sim_noise, incidence_angles = thermal_noise.compute_simulated_noise(az_central_time)
123
+
124
+ start_az_time = az_t0.isoformat(sep="T", timespec="microseconds").rstrip("Z")
125
+
126
+ sim_noises.update({
127
+ (rel_burst, start_az_time): (sim_noise, geo_grid_info, incidence_angles, dn_value)
128
+ })
129
+
130
+ # Append it
131
+ simulated_noises.append(OutputNoiseVectors_SLC(adsHeader=ads_header, noiseRangeVectors=sim_noises))
132
+
133
+ return simulated_noises
134
+
135
+ def grd_processing(safe, aux_files, dn_value, noise_power_correction):
136
+ """
137
+ Generate the simulated noise range vectors of a GRD product.
138
+
139
+ For each polarization, the subswaths merged in the annotation are processed separately: one noise vector is
140
+ computed in slant range for each antenna pattern record (block) of the subswath and converted to ground range.
141
+ The subswaths are then merged into cross-subswath noise vectors by `OutputNoiseVectors_GRD`.
142
+
143
+ Parameters
144
+ ----------
145
+ safe : S1Safe
146
+ Input GRD SAFE product.
147
+ aux_files : AuxiliaryFiles
148
+ Auxiliary files used for processing.
149
+ dn_value : float
150
+ Digital number calibration value from AUX_PP1, used to scale the output noise values.
151
+ noise_power_correction : dict or None
152
+ Noise power extracted from the L0N product, mapping ``(swath, polarization)`` to
153
+ ``[azimuth_times, noise_powers]``, or None to use the noise power annotated in the product.
154
+
155
+ Returns
156
+ -------
157
+ list of OutputNoiseVectors_GRD
158
+ Output noise vectors, one element per polarization.
159
+ """
160
+
161
+ simulated_noises = []
162
+
163
+ for pol, swath in safe._channels:
164
+
165
+ channel = safe.channel(pol, swath)
166
+ denoise_logger.info(f"Processing bursts from polarization {pol}, subswath {swath}")
167
+
168
+ metadata_dict = read_metadata_from_safe_annotation(channel.annotation)
169
+ ads_header = metadata_dict['product']['adsHeader']
170
+
171
+ count_noise_list = int(channel.annotation.find('generalAnnotation/noiseList').attrib['count'])
172
+ antenna_patterns = channel.annotation.findall('antennaPattern/antennaPatternList/antennaPattern')
173
+ tot_noise_vectors_out = int(channel.annotation.find('antennaPattern/antennaPatternList').attrib['count'])
174
+ swath_merging_info = ensure_list(metadata_dict['product']['swathMerging']['swathMergeList']['swathMerge'])
175
+
176
+ # for slant to range conversion
177
+ raw_cc = metadata_dict['product']['coordinateConversion']['coordinateConversionList']['coordinateConversion']
178
+ coordinate_conversions = []
179
+ for cc in raw_cc:
180
+ coordinate_conversions.append({
181
+ "azimuthTime": cc["azimuthTime"],
182
+ "sr0": float(cc["sr0"]),
183
+ "gr0": float(cc["gr0"]), # adjust if needed
184
+ "srgrCoeffs": [float(x) for x in cc["srgrCoefficients"]["#text"].split()]
185
+ })
186
+
187
+ # separate noise by subswath
188
+ noise_subswaths = []
189
+ for idx in range(1, count_noise_list+1):
190
+ tmp_subswath = channel.annotation.find('generalAnnotation/noiseList/noise[{}]/swath'.format(idx)).text
191
+ if tmp_subswath not in noise_subswaths:
192
+ noise_subswaths.append(tmp_subswath)
193
+
194
+ sim_noises = {}
195
+
196
+ for subswath in noise_subswaths:
197
+
198
+ thermal_noise = ThermalNoise(metadata_dict=metadata_dict,
199
+ channel=channel,
200
+ aux_info=aux_files,
201
+ product_id=safe.path.stem[4:13],
202
+ noise_power_from_l0=noise_power_correction,
203
+ subswath=subswath)
204
+
205
+ subswath_antenna_patterns = [
206
+ ap for ap in antenna_patterns
207
+ if ap.findtext('swath') == subswath
208
+ ]
209
+
210
+ denoise_logger.info(f"Generating noise profiles for subswath {subswath} ({pol})")
211
+
212
+ for index in range(len(subswath_antenna_patterns)):
213
+
214
+ denoise_logger.debug(f"Generating noise profiles for block {index+1} ({pol}, {subswath})")
215
+
216
+ az_t0 = PreciseDateTime.from_utc_string(subswath_antenna_patterns[index].findtext('azimuthTime'))
217
+
218
+ range_t = create_slant_range_times(metadata_dict, subswath=subswath, az_t0=az_t0)
219
+
220
+ # create simulated noise
221
+ thermal_noise.set_range_values(range_t)
222
+ thermal_noise.rel_burst = index
223
+ thermal_noise.get_shrink_factor()
224
+
225
+ # slant range simulated noise
226
+ sim_noise, incidence_angles = thermal_noise.compute_simulated_noise(az_t0)
227
+
228
+ # convert from slant range to ground range using actual SR distances
229
+ az_t0_string = az_t0.isoformat(sep="T", timespec="microseconds").rstrip("Z")
230
+ sim_noise, incidence_angles = slant2ground(
231
+ az_t0_string, sim_noise, incidence_angles,
232
+ coordinate_conversions, thermal_noise.range_distances
233
+ )
234
+
235
+ sim_noises.update({
236
+ (index, subswath): (az_t0_string, sim_noise, incidence_angles)
237
+ })
238
+
239
+ # append it
240
+ simulated_noises.append(OutputNoiseVectors_GRD(adsHeader=ads_header,
241
+ swath_merges=swath_merging_info,
242
+ noiseRangeVectors=sim_noises,
243
+ dn_value=dn_value,
244
+ tot_vec_out=tot_noise_vectors_out))
245
+
246
+ return simulated_noises
247
+
248
+ def generate_noise_profiles(prod: Path, aux_files: AuxiliaryFiles, l0n_file: Path = None):
249
+ """
250
+ Generate the simulated noise range vectors of a Sentinel-1 L1 product.
251
+
252
+ Dispatches to `slc_processing` or `grd_processing` depending on the product type. If an L0N product is given,
253
+ the noise power is extracted from it instead of being read from the product annotation.
254
+
255
+ Parameters
256
+ ----------
257
+ prod : pathlib.Path
258
+ Path to the input L1 SAFE product (SLC or GRD).
259
+ aux_files : AuxiliaryFiles
260
+ Auxiliary files used for processing.
261
+ l0n_file : pathlib.Path, optional
262
+ Path to the associated L0N SAFE product. If None (default), the annotated noise power is used.
263
+
264
+ Returns
265
+ -------
266
+ list of OutputNoiseVectors_SLC or list of OutputNoiseVectors_GRD
267
+ Output noise vectors, one element per output noise file.
268
+
269
+ Notes
270
+ -----
271
+ The process exits with code 128 if the product type is neither SLC nor GRD.
272
+ """
273
+
274
+ safe = S1Safe(prod)
275
+ dn_value = aux_files.get_dn_value(safe.mode, safe.product_type)
276
+
277
+ denoise_logger.info(f"Denoising product: {prod.resolve()}")
278
+
279
+ if l0n_file is not None:
280
+ noise_power_correction = get_noise_pwr_from_l0n(l0n_path = l0n_file, aux_ins_path = aux_files.ins_filepath)
281
+ else:
282
+ noise_power_correction = None
283
+
284
+ if safe.product_type == "SLC":
285
+ simulated_noises = slc_processing(safe, aux_files, dn_value, noise_power_correction)
286
+ elif safe.product_type.startswith("GRD"):
287
+ simulated_noises = grd_processing(safe, aux_files, dn_value, noise_power_correction)
288
+ else:
289
+ denoise_logger.error(f"Unknown product type: {safe.product_type}")
290
+ sys.exit(128)
291
+
292
+ return simulated_noises