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 ADDED
@@ -0,0 +1,18 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Configuration and constants module for the S1 L1N processor.
6
+
7
+ Defines the package version, the processor name used in log records, the reference slant range ``R_REF``
8
+ (in meters) and the noise pulse outlier detection bounds ``NOISE_LOWER_BOUND`` / ``NOISE_UPPER_BOUND`` (in dB).
9
+ """
10
+
11
+ __version__ = '0.0.0.dev0'
12
+
13
+ PROCESSOR_NAME = 's1_l1n_processor'
14
+
15
+ # CONSTANTS
16
+ R_REF = 8e5 # [m]
17
+
18
+ NOISE_LOWER_BOUND, NOISE_UPPER_BOUND = 3.0, 1.0 # [dB] bounds for noise pulse outlier detection
s1_denoise/__main__.py ADDED
@@ -0,0 +1,36 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Entry point module for the denoising command-line interface."""
5
+
6
+ import sys
7
+ import click
8
+
9
+ from s1_denoise.cli import denoise_cli
10
+
11
+ def main():
12
+ """
13
+ Run the denoising CLI, mapping Click errors to exit code 128.
14
+
15
+ The command is invoked with ``standalone_mode=False`` so that Click usage errors and user aborts are
16
+ reported here and terminate the processor with exit code 128 instead of Click's default exit codes.
17
+
18
+ Raises
19
+ ------
20
+ SystemExit
21
+ With code 128 on a Click usage error or abort; exit codes raised by the CLI itself are propagated.
22
+ """
23
+ # standalone_mode=False lets click errors (e.g. invalid arguments) propagate,
24
+ # so that the processor exits with code 128 also in these cases
25
+ try:
26
+ denoise_cli(standalone_mode=False)
27
+ except click.ClickException as e:
28
+ e.show()
29
+ sys.exit(128)
30
+ except click.Abort:
31
+ click.echo("Aborted!", err=True)
32
+ sys.exit(128)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,481 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Provide the AuxiliaryFiles class for parsing and accessing the Sentinel-1 auxiliary files (AUX_CAL, AUX_PP1,
6
+ AUX_INS).
7
+
8
+ Handles the reading of calibration parameters, processing parameters, application LUTs and internal calibration
9
+ references, exposing them through dedicated getter methods for use in the denoising pipeline.
10
+ """
11
+
12
+ from lxml import etree
13
+ from pathlib import Path
14
+ import numpy as np
15
+ import sys
16
+
17
+ from s1_denoise.logger import denoise_logger
18
+
19
+ class AuxiliaryFiles:
20
+ """
21
+ Parsed content of the AUX_PP1, AUX_CAL and AUX_INS auxiliary files of a mission.
22
+
23
+ All three files are read at construction time.
24
+
25
+ Parameters
26
+ ----------
27
+ mission_id : str
28
+ Mission identifier (e.g. ``"S1A"``), used to build the auxiliary file names.
29
+ pp1_path : pathlib.Path
30
+ Path of the AUX_PP1 SAFE directory, containing ``data/<mission>-aux-pp1.xml``.
31
+ cal_path : pathlib.Path
32
+ Path of the AUX_CAL SAFE directory, containing ``data/<mission>-aux-cal.xml``.
33
+ ins_path : pathlib.Path
34
+ Path of the AUX_INS SAFE directory, containing ``data/<mission>-aux-ins.xml``.
35
+
36
+ Attributes
37
+ ----------
38
+ mission_id : str
39
+ Mission identifier.
40
+ pp1_filepath, cal_filepath, ins_filepath : pathlib.Path
41
+ Paths of the AUX_PP1, AUX_CAL and AUX_INS XML files.
42
+ calibration_parameters : dict
43
+ AUX_CAL calibration parameters, keyed by ``(swath, polarisation)``.
44
+ processing_parameters : dict
45
+ AUX_PP1 processing parameters (``"gain"`` and ``"alpha_value"``), keyed by ``(product_id, swath)``.
46
+ app_lut_values : dict
47
+ AUX_PP1 scaling LUTs, keyed by application LUT identifier.
48
+ pg_references : dict
49
+ AUX_INS ``pgReference`` XML elements, keyed by ``(swath, polarisation)``.
50
+ expected_noise_values : dict
51
+ AUX_INS expected ``noise`` XML elements, keyed by ``(swath, polarisation)``.
52
+ azimuth_steering_rates : dict
53
+ AUX_INS azimuth steering rates [deg/s], keyed by swath.
54
+
55
+ Raises
56
+ ------
57
+ SystemExit
58
+ If any of the auxiliary files contains no usable entries (exit code 128).
59
+ """
60
+
61
+ def __init__(self, mission_id, pp1_path: Path, cal_path: Path, ins_path: Path):
62
+
63
+ self.mission_id = mission_id
64
+ self.pp1_filepath = pp1_path / f'data/{self.mission_id.lower()}-aux-pp1.xml'
65
+ self.cal_filepath = cal_path / f'data/{self.mission_id.lower()}-aux-cal.xml'
66
+ self.ins_filepath = ins_path / f'data/{self.mission_id.lower()}-aux-ins.xml'
67
+
68
+ self.calibration_parameters = {}
69
+ self.read_from_aux_cal()
70
+
71
+ self.processing_parameters = {}
72
+ self.app_lut_values = {}
73
+ self.read_from_aux_pp1()
74
+
75
+ self.pg_references = {}
76
+ self.expected_noise_values = {}
77
+ self.azimuth_steering_rates = {}
78
+ self.read_from_aux_ins()
79
+
80
+
81
+ def read_from_aux_cal(self):
82
+ """
83
+ Read the calibration parameters from the AUX_CAL file into `calibration_parameters`.
84
+
85
+ For each ``(swath, polarisation)``, the azimuth antenna pattern values and count, the azimuth angle increment, the
86
+ noise calibration factor and the absolute calibration constant are stored.
87
+
88
+ Raises
89
+ ------
90
+ SystemExit
91
+ If no calibration parameters are found (exit code 128).
92
+ """
93
+
94
+ tree = etree.parse(self.cal_filepath.resolve())
95
+ root = tree.getroot()
96
+
97
+ for cal_params in root.findall(".//calibrationParams"):
98
+ swath = cal_params.find("swath").text
99
+ polarisation = cal_params.find("polarisation").text
100
+ aap_values = cal_params.find("azimuthAntennaPattern/values")
101
+ aap_values_text = aap_values.text.strip()
102
+
103
+ self.calibration_parameters[(swath, polarisation)] = {
104
+ "aap_gain_count": int(aap_values.attrib['count']),
105
+ "aap_gain": [float(val) for val in aap_values_text.split()],
106
+ "azi_angle_incr": float(cal_params.find("azimuthAntennaPattern/azimuthAngleIncrement").text),
107
+ "noise_calibration_factor": float(cal_params.find("noiseCalibrationFactor").text),
108
+ "absolute_calibration_constant": float(cal_params.find("absoluteCalibrationConstant").text)
109
+ }
110
+
111
+ # if no matching swath/polarisation is found
112
+ if not self.calibration_parameters:
113
+ denoise_logger.error(f"Error reading calibration file. Aborting...")
114
+ sys.exit(128)
115
+
116
+ def read_from_aux_pp1(self):
117
+ """
118
+ Read the application LUTs and the processing parameters from the AUX_PP1 file.
119
+
120
+ Scaling LUTs are stored in `app_lut_values`; the per-swath gains (``swathParams``) and azimuth window coefficients
121
+ (``azimuthParams``) are stored in `processing_parameters`.
122
+
123
+ Raises
124
+ ------
125
+ SystemExit
126
+ If no processing parameters are found (exit code 128).
127
+ """
128
+
129
+ tree = etree.parse(self.pp1_filepath.resolve())
130
+ root = tree.getroot()
131
+
132
+ for app_lut in root.findall(".//applicationLut"):
133
+ app_id = app_lut.findtext("applicationLutId")
134
+ scaling_luts = []
135
+
136
+ for lut in app_lut.findall("scalingLutList/scalingLut"):
137
+ output_pixels = lut.findtext("outputPixels")
138
+ inc_angle_start = float(lut.findtext("incidenceAngleStart"))
139
+ angle_increment = float(lut.findtext("angleIncrement"))
140
+
141
+ # Split and convert numeric values
142
+ values_text = lut.findtext("values")
143
+ values = [float(v) for v in values_text.split()] if values_text else []
144
+
145
+ scaling_luts.append({
146
+ "outputPixels": output_pixels,
147
+ "incidenceAngleStart": inc_angle_start,
148
+ "angleIncrement": angle_increment,
149
+ "values": values
150
+ })
151
+
152
+ self.app_lut_values.update({
153
+ app_id: scaling_luts
154
+ })
155
+
156
+ # Find the calibrationParams
157
+ for product in root.findall(".//product"):
158
+ prod_id = product.find("productId").text
159
+
160
+ for swath_param in product.findall(".//swathParams"): # swathParams is present only in slcProcParams
161
+ swath = swath_param.find("swath").text
162
+
163
+ # for pol in pp1_gain_map:
164
+ self.processing_parameters[(prod_id, swath)] = {
165
+ "gain": list(swath_param.find("gain").text.split())
166
+ }
167
+
168
+ for azi_param in product.findall('.//azimuthParams'): # azimuthParams is present only in postProcParams
169
+ swath = azi_param.find("swath").text
170
+ self.processing_parameters[(prod_id, swath)].update({
171
+ "alpha_value": float(azi_param.find('windowCoefficient').text)
172
+ })
173
+
174
+
175
+ # if no matching swath/polarisation is found
176
+ if not self.processing_parameters:
177
+ denoise_logger.error(f"Error reading PP1 file. Aborting...")
178
+ sys.exit(128)
179
+
180
+ def read_from_aux_ins(self):
181
+ """
182
+ Read the internal calibration parameters and azimuth steering rates from the AUX_INS file.
183
+
184
+ PG references and expected noise values are stored in `pg_references` and `expected_noise_values`; azimuth
185
+ steering rates [deg/s] in `azimuth_steering_rates`.
186
+
187
+ Raises
188
+ ------
189
+ SystemExit
190
+ If no internal calibration parameters are found (exit code 128).
191
+ """
192
+
193
+ tree = etree.parse(self.ins_filepath.resolve())
194
+ root = tree.getroot()
195
+
196
+ for int_cal_param in root.findall(".//internalCalibrationParams"):
197
+ swath = int_cal_param.findtext("swath")
198
+ pol = int_cal_param.findtext("polarisation")
199
+ pg_ref = int_cal_param.find("pgReference")
200
+ expected_noise = int_cal_param.find("noise")
201
+
202
+ self.pg_references.update({
203
+ (swath, pol): pg_ref
204
+ })
205
+
206
+ self.expected_noise_values.update({
207
+ (swath, pol): expected_noise
208
+ })
209
+
210
+ for swath_param in root.findall(".//swathParams"):
211
+ swath = swath_param.findtext("swath")
212
+ steering_rate = swath_param.findtext("radarParams/azimuthSteeringRate")
213
+
214
+ if steering_rate is not None:
215
+ self.azimuth_steering_rates.update({
216
+ swath: float(steering_rate) # [deg/s]
217
+ })
218
+
219
+ # if no matching swath/polarisation is found
220
+ if not self.pg_references:
221
+ denoise_logger.error(f"Error reading INS file. Aborting...")
222
+ sys.exit(128)
223
+
224
+ def get_gain_from_pp1(self, product_id, subswath, pol):
225
+ """
226
+ Return the processing gain from AUX_PP1 for a product type, subswath and polarization.
227
+
228
+ Parameters
229
+ ----------
230
+ product_id : str
231
+ AUX_PP1 product identifier (e.g. ``"IW_SLC__1"``).
232
+ subswath : str
233
+ Subswath identifier.
234
+ pol : str
235
+ Polarization, one of ``"HH"``, ``"HV"``, ``"VV"``, ``"VH"``.
236
+
237
+ Returns
238
+ -------
239
+ float
240
+ Gain value.
241
+ """
242
+
243
+ pp1_gain_map = {
244
+ "HH": 0,
245
+ "HV": 1,
246
+ "VV": 2,
247
+ "VH": 3
248
+ }
249
+
250
+ gain_value = float(self.processing_parameters.get((product_id, subswath))['gain'][pp1_gain_map[pol]])
251
+ denoise_logger.debug(f"Gain value from AUX_PP1: {gain_value}")
252
+
253
+ return gain_value
254
+
255
+ def get_alpha_from_pp1(self, product_id, subswath):
256
+ """
257
+ Return the azimuth window coefficients (``windowCoefficient``) from AUX_PP1.
258
+
259
+ Parameters
260
+ ----------
261
+ product_id : str
262
+ AUX_PP1 product identifier (e.g. ``"IW_GRDH_1"``).
263
+ subswath : str
264
+ Subswath identifier.
265
+
266
+ Returns
267
+ -------
268
+ alpha_value : float
269
+ Window coefficient of the given product type.
270
+ slc_alpha_value : float
271
+ Window coefficient of the SLC product of the same mode (``<mode>_SLC__1``).
272
+ """
273
+
274
+ alpha_value = float(self.processing_parameters.get((product_id, subswath))['alpha_value'])
275
+ slc_alpha_value = float(self.processing_parameters.get((product_id[:2] + '_SLC__1', subswath))['alpha_value'])
276
+ denoise_logger.debug(f"Window coefficient for {product_id}, {subswath}: {alpha_value}")
277
+
278
+ return alpha_value, slc_alpha_value
279
+
280
+ def get_abs_cal(self, swath: str, polarisation: str) -> float:
281
+ """
282
+ Return the absolute calibration constant from AUX_CAL.
283
+
284
+ Parameters
285
+ ----------
286
+ swath : str
287
+ Swath identifier.
288
+ polarisation : str
289
+ Polarization (e.g. ``"VV"``).
290
+
291
+ Returns
292
+ -------
293
+ float
294
+ Absolute calibration constant.
295
+ """
296
+
297
+ acf = float(self.calibration_parameters.get((swath, polarisation))['absolute_calibration_constant'])
298
+ return acf
299
+
300
+ def get_noise_cal(self, swath: str, polarisation: str) -> float:
301
+ """
302
+ Return the noise calibration factor k_N from AUX_CAL.
303
+
304
+ Parameters
305
+ ----------
306
+ swath : str
307
+ Swath identifier.
308
+ polarisation : str
309
+ Polarization (e.g. ``"VV"``).
310
+
311
+ Returns
312
+ -------
313
+ float
314
+ Noise calibration factor.
315
+ """
316
+
317
+ k_n = float(self.calibration_parameters.get((swath, polarisation))['noise_calibration_factor'])
318
+ return k_n
319
+
320
+ def get_aap_gain(self, swath, polarisation):
321
+ """
322
+ Return the azimuth antenna pattern values from AUX_CAL.
323
+
324
+ Parameters
325
+ ----------
326
+ swath : str
327
+ Swath identifier.
328
+ polarisation : str
329
+ Polarization (e.g. ``"VV"``).
330
+
331
+ Returns
332
+ -------
333
+ numpy.ndarray
334
+ Azimuth antenna pattern values, as annotated (dB).
335
+ """
336
+
337
+ aap_gain = np.array(self.calibration_parameters.get((swath, polarisation))['aap_gain'])
338
+ return aap_gain
339
+
340
+ def get_aap_gain_count(self, swath, polarisation):
341
+ """
342
+ Return the number of azimuth antenna pattern values from AUX_CAL.
343
+
344
+ Parameters
345
+ ----------
346
+ swath : str
347
+ Swath identifier.
348
+ polarisation : str
349
+ Polarization (e.g. ``"VV"``).
350
+
351
+ Returns
352
+ -------
353
+ int
354
+ Number of azimuth antenna pattern values.
355
+ """
356
+
357
+ aap_gain_count = int(self.calibration_parameters.get((swath, polarisation))['aap_gain_count'])
358
+ return aap_gain_count
359
+
360
+ def get_azi_angle_incr(self, swath, polarisation):
361
+ """
362
+ Return the azimuth angle increment of the azimuth antenna pattern from AUX_CAL.
363
+
364
+ Parameters
365
+ ----------
366
+ swath : str
367
+ Swath identifier.
368
+ polarisation : str
369
+ Polarization (e.g. ``"VV"``).
370
+
371
+ Returns
372
+ -------
373
+ float
374
+ Azimuth angle increment, as annotated.
375
+ """
376
+
377
+ azi_angle_incr = float(self.calibration_parameters.get((swath, polarisation))['azi_angle_incr'])
378
+ return azi_angle_incr
379
+
380
+ def get_dn_value(self, mode, product_type):
381
+ """
382
+ Return the digital number scaling value (betaNought * calibrationFactor) from the AUX_PP1 application LUT.
383
+
384
+ The first value of the ``<mode>_Default`` scaling LUT matching the output pixel type of the product is returned:
385
+ 16 bit signed integer for SLC, 16 bit unsigned integer for GRD, 8 bit unsigned integer otherwise.
386
+
387
+ Parameters
388
+ ----------
389
+ mode : str
390
+ Acquisition mode (e.g. ``"IW"``).
391
+ product_type : str
392
+ Product type (e.g. ``"SLC"``, ``"GRDH"``).
393
+
394
+ Returns
395
+ -------
396
+ float
397
+ Digital number value, used to scale the output noise vectors.
398
+ """
399
+
400
+ if product_type == "SLC":
401
+ output_pixels = "16 bit Signed Integer"
402
+ elif product_type.startswith("GRD"):
403
+ output_pixels = "16 bit Unsigned Integer"
404
+ else:
405
+ output_pixels = "8 bit Unsigned Integer"
406
+
407
+ dn_value = next(
408
+ (d["values"][0] for d in self.app_lut_values[f"{mode}_Default"] if d["outputPixels"] == output_pixels),
409
+ None
410
+ )
411
+
412
+
413
+ return float(dn_value)
414
+
415
+ def get_pg_reference(self, swath, polarisation):
416
+ """
417
+ Return the magnitude of the PG reference from AUX_INS.
418
+
419
+ Parameters
420
+ ----------
421
+ swath : str
422
+ Swath identifier.
423
+ polarisation : str
424
+ Polarization (e.g. ``"VV"``).
425
+
426
+ Returns
427
+ -------
428
+ float
429
+ Euclidean norm of the two ``pgReference`` components.
430
+ """
431
+
432
+ val = np.array([float(self.pg_references.get((swath, polarisation))[0].text),
433
+ float(self.pg_references.get((swath, polarisation))[1].text)])
434
+
435
+ return np.linalg.norm(val)
436
+
437
+ def get_expected_noise_value(self, swath, polarisation):
438
+ """
439
+ Return the expected noise power from AUX_INS, used as reference for noise pulse outlier rejection.
440
+
441
+ Parameters
442
+ ----------
443
+ swath : str
444
+ Swath identifier.
445
+ polarisation : str
446
+ Polarization (e.g. ``"VV"``).
447
+
448
+ Returns
449
+ -------
450
+ float
451
+ Expected noise power (linear scale).
452
+ """
453
+
454
+ val = float(self.expected_noise_values.get((swath, polarisation)).text)
455
+
456
+ return val
457
+
458
+ def get_azimuth_steering_rate(self, swath):
459
+ """
460
+ Return the azimuth antenna steering rate from AUX_INS.
461
+
462
+ Parameters
463
+ ----------
464
+ swath : str
465
+ Swath identifier.
466
+
467
+ Returns
468
+ -------
469
+ float
470
+ Azimuth steering rate [rad/s].
471
+
472
+ Raises
473
+ ------
474
+ ValueError
475
+ If no steering rate is annotated for `swath`.
476
+ """
477
+
478
+ if swath not in self.azimuth_steering_rates:
479
+ raise ValueError(f"Azimuth steering rate for swath {swath} not found in AUX_INS file")
480
+
481
+ return np.deg2rad(self.azimuth_steering_rates[swath])