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.
@@ -0,0 +1,571 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Parse and extract geophysical parameters from Sentinel-1 L1 annotation XML files.
6
+
7
+ Provides readers for noise power, antenna patterns (elevation pattern, incidence angles), PG correction, azimuth FM
8
+ rate and orbit state vectors, as well as helpers to build the slant range time axis and the orbit trajectory, and to
9
+ convert the azimuth antenna steering rate into a Doppler rate.
10
+ """
11
+
12
+ import numpy as np
13
+ import xml.etree.ElementTree as ET
14
+ import xmltodict
15
+ from scipy.constants import speed_of_light
16
+ from perseo_core.timing import PreciseDateTime
17
+ from perseo_core.geometry.navigation import CubicSplineTrajectory
18
+
19
+ from s1_denoise.utils import ground2slant, ensure_list, get_range_boundaries
20
+ from s1_denoise.logger import denoise_logger
21
+
22
+ def read_metadata_from_safe_annotation(annotation):
23
+ """
24
+ Convert a parsed L1 annotation XML tree into a nested dictionary.
25
+
26
+ Parameters
27
+ ----------
28
+ annotation : xml.etree.ElementTree.ElementTree
29
+ Parsed annotation XML of one channel.
30
+
31
+ Returns
32
+ -------
33
+ dict
34
+ Nested dictionary produced by ``xmltodict``, rooted at the ``product`` key.
35
+ """
36
+
37
+ xml_data = annotation.getroot()
38
+ xmlstr = ET.tostring(xml_data, encoding='utf-8', method='xml')
39
+
40
+ return dict(xmltodict.parse(xmlstr))
41
+
42
+ def create_slant_range_times(metadata_dict, subswath = None, az_t0 = None):
43
+ """
44
+ Create the two-way slant range time axis of the image range samples.
45
+
46
+ Parameters
47
+ ----------
48
+ metadata_dict : dict
49
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
50
+ subswath : str, optional
51
+ Subswath identifier (e.g. ``"IW1"``), to be given for GRD products only. Default is None (SLC case).
52
+ az_t0 : PreciseDateTime, optional
53
+ Azimuth time used to select the swath bounds and the ground-to-slant range polynomial. Required when
54
+ `subswath` is given. Default is None.
55
+
56
+ Returns
57
+ -------
58
+ numpy.ndarray
59
+ Two-way slant range times [s], one per range sample.
60
+
61
+ Notes
62
+ -----
63
+ In the SLC case, ``numberOfSamples`` times are generated starting from ``slantRangeTime`` with a step of
64
+ ``2 * rangePixelSpacing / c``. In the GRD case, the ground range pixels between the first and last range samples of
65
+ the subswath (from the swath merging bounds valid at `az_t0`) are converted to ground range distances using
66
+ ``rangePixelSpacing``, then to slant range with the GRSR polynomial closest in azimuth to `az_t0`, and finally to
67
+ two-way slant range times.
68
+ """
69
+
70
+ delta_t = 2 * (float(metadata_dict['product']['imageAnnotation']['imageInformation']['rangePixelSpacing'])) / speed_of_light
71
+ start_sr = float(metadata_dict['product']['imageAnnotation']['imageInformation']['slantRangeTime'])
72
+ samples_num = int(metadata_dict['product']['imageAnnotation']['imageInformation']['numberOfSamples'])
73
+
74
+ # SLC case
75
+ if subswath is None:
76
+ return np.arange(samples_num) * delta_t + start_sr # [s]
77
+ # GRD case
78
+ else:
79
+ swath_merges = ensure_list(metadata_dict['product']['swathMerging']['swathMergeList']['swathMerge'])
80
+ cc = metadata_dict['product']['coordinateConversion']['coordinateConversionList']['coordinateConversion']
81
+
82
+ start_sample, end_sample = get_range_boundaries(swath_merges, subswath, az_t0)
83
+
84
+ # GRD pixel indices for the subswath
85
+ indices = np.arange(start_sample, end_sample + 1)
86
+
87
+ # ground range times [s]
88
+ t_grd = indices * delta_t
89
+
90
+ # ground range [m]
91
+ gr_m = t_grd * speed_of_light / 2
92
+
93
+ # slant range [m]
94
+ sr_m = ground2slant(start_az_time = az_t0.isoformat(sep="T", timespec="microseconds").rstrip("Z"),
95
+ gr_array = gr_m,
96
+ coordinate_conversions = cc)
97
+
98
+ # slant range times [s]
99
+ t_slant = sr_m * 2 / speed_of_light
100
+
101
+ return t_slant
102
+
103
+ def read_pg_correction_list(annotation, swath_id):
104
+ """
105
+ Read the PG product amplitudes annotated in the replica information of a swath.
106
+
107
+ Parameters
108
+ ----------
109
+ annotation : xml.etree.ElementTree.ElementTree
110
+ Parsed annotation XML of one channel.
111
+ swath_id : str
112
+ Swath identifier (e.g. ``"IW1"``).
113
+
114
+ Returns
115
+ -------
116
+ pg_correction : numpy.ndarray
117
+ ``pgProductAmplitude`` values, one per replica record.
118
+ az_t : numpy.ndarray
119
+ Azimuth times of the replica records, as PreciseDateTime objects.
120
+ """
121
+
122
+ pg_correction = []
123
+ az_t = []
124
+
125
+ data = annotation.findall('generalAnnotation/replicaInformationList/replicaInformation/swath')
126
+ for ind, element in enumerate(data):
127
+ if element.text == swath_id:
128
+
129
+ pg_correction_list = annotation.findall('generalAnnotation/replicaInformationList/replicaInformation[{}]/replicaList/replica/pgProductAmplitude'.format(ind+1))
130
+ az_t_list = annotation.findall('generalAnnotation/replicaInformationList/replicaInformation[{}]/replicaList/replica/azimuthTime'.format(ind+1))
131
+
132
+ for t_ind, t in enumerate(az_t_list):
133
+ pg_correction.append(float(pg_correction_list[t_ind].text))
134
+ az_t.append(t.text)
135
+
136
+
137
+ return np.array(pg_correction), np.array(convert_az_times(az_t))
138
+
139
+ def read_noise_power_correction_from_annotation(metadata_dict, swath_id):
140
+ """
141
+ Read the noise power annotated in the L1 product for a swath, discarding biased rank echo measurements.
142
+
143
+ Parameters
144
+ ----------
145
+ metadata_dict : dict
146
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
147
+ swath_id : str
148
+ Swath identifier (e.g. ``"IW1"``).
149
+
150
+ Returns
151
+ -------
152
+ final_noise_pwr : numpy.ndarray
153
+ Retained ``noisePowerCorrectionFactor`` values (linear scale).
154
+ final_times : numpy.ndarray
155
+ Azimuth times of the retained values, as PreciseDateTime objects.
156
+
157
+ Notes
158
+ -----
159
+ The same mission-specific selection applied to L0N rank echoes (see
160
+ `s1_denoise.l0_noise_power_calculation.filter_rank_echoes`) is used. For S1A/B: IW1 keeps the first value, the
161
+ odd-indexed values and the last value; IW2/IW3 keep the even-indexed values and the last value; EW1 drops the last
162
+ value. For the other missions, the last EW2 value is dropped. All other swaths are kept unchanged.
163
+ """
164
+
165
+ data = metadata_dict['product']['generalAnnotation']['noiseList']['noise']
166
+ mission_id = metadata_dict['product']['adsHeader']['missionId']
167
+
168
+ filtered = [el for el in data if el['swath'] == swath_id]
169
+ noise_power_correction = np.array([float(el['noisePowerCorrectionFactor']) for el in filtered])
170
+ az_t = np.array([el['azimuthTime'] for el in filtered])
171
+
172
+ if mission_id in ["S1A", "S1B"]:
173
+ if swath_id == "IW1":
174
+ final_noise_pwr = np.concatenate([[noise_power_correction[0]], noise_power_correction[1::2], [noise_power_correction[-1]]])
175
+ final_times = np.concatenate([[az_t[0]], az_t[1::2], [az_t[-1]]])
176
+ elif swath_id in ["IW2", "IW3"]:
177
+ final_noise_pwr = np.concatenate([noise_power_correction[0::2], [noise_power_correction[-1]]])
178
+ final_times = np.concatenate([az_t[0::2], [az_t[-1]]])
179
+ elif swath_id == "EW1":
180
+ final_noise_pwr = noise_power_correction[:-1]
181
+ final_times = az_t[:-1]
182
+ else:
183
+ final_times = az_t
184
+ final_noise_pwr = noise_power_correction
185
+ else:
186
+ if swath_id == "EW2":
187
+ final_times = az_t[:-1]
188
+ final_noise_pwr = noise_power_correction[:-1]
189
+ else:
190
+ final_times = az_t
191
+ final_noise_pwr = noise_power_correction
192
+
193
+ return final_noise_pwr, convert_az_times(final_times)
194
+
195
+ def read_antenna_patterns(metadata_dict, swath_id):
196
+ """
197
+ Parse the antenna pattern records of a swath.
198
+
199
+ Parameters
200
+ ----------
201
+ metadata_dict : dict
202
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
203
+ swath_id : str
204
+ Swath identifier (e.g. ``"IW1"``).
205
+
206
+ Returns
207
+ -------
208
+ dict
209
+ Dictionary with one element per antenna pattern record of the swath, under the keys:
210
+
211
+ - ``"azimuth_times"``: numpy.ndarray of PreciseDateTime record azimuth times;
212
+ - ``"elevation_pattern_gain"``: list of numpy.ndarray, magnitude of the annotated complex elevation pattern;
213
+ - ``"incidence_angles"``: list of numpy.ndarray, incidence angles [deg];
214
+ - ``"slant_range_t"``: list of numpy.ndarray, slant range times [s] at which the above are given.
215
+ """
216
+
217
+ data = ensure_list(metadata_dict['product']['antennaPattern']['antennaPatternList']['antennaPattern'])
218
+
219
+ antenna_patterns = {
220
+ "azimuth_times": [],
221
+ "elevation_pattern_gain": [],
222
+ "incidence_angles": [],
223
+ "slant_range_t": []
224
+ }
225
+
226
+ for element in data:
227
+ if element['swath'] == swath_id:
228
+
229
+ antenna_patterns["azimuth_times"].append(PreciseDateTime.from_utc_string(element['azimuthTime']))
230
+
231
+ elevation = np.array(element['elevationPattern']['#text'].split(' '), dtype=float)
232
+ antenna_patterns["elevation_pattern_gain"].append(abs(elevation[::2] + elevation[1::2] * 1j))
233
+
234
+ antenna_patterns["incidence_angles"].append(np.array(element['incidenceAngle']['#text'].split(' '), dtype=float))
235
+ antenna_patterns["slant_range_t"].append(np.array(element['slantRangeTime']['#text'].split(' '), dtype=float))
236
+
237
+ antenna_patterns["azimuth_times"] = np.array(antenna_patterns["azimuth_times"])
238
+
239
+ return antenna_patterns
240
+
241
+ def closest_antenna_pattern_index(antenna_patterns, az_t):
242
+ """
243
+ Return the index of the antenna pattern record closest in azimuth time to `az_t`.
244
+
245
+ Parameters
246
+ ----------
247
+ antenna_patterns : dict
248
+ Antenna patterns, as returned by `read_antenna_patterns`.
249
+ az_t : PreciseDateTime
250
+ Reference azimuth time.
251
+
252
+ Returns
253
+ -------
254
+ int
255
+ Index of the closest record.
256
+ """
257
+
258
+ return np.argmin(abs(antenna_patterns["azimuth_times"] - az_t))
259
+
260
+ def extract_AR_info(metadata_dict, az_t, range_times, az_steering_rate, orbit = None):
261
+ """
262
+ Compute the Doppler rate induced by the azimuth antenna steering, constant over the range axis.
263
+
264
+ Parameters
265
+ ----------
266
+ metadata_dict : dict
267
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
268
+ az_t : PreciseDateTime
269
+ Azimuth time at which the satellite velocity is evaluated.
270
+ range_times : numpy.ndarray
271
+ Slant range times [s]; only their number is used.
272
+ az_steering_rate : float
273
+ Azimuth antenna steering rate of the subswath [rad/s], from AUX_INS.
274
+ orbit : CubicSplineTrajectory, optional
275
+ Orbit trajectory, to avoid rebuilding it from the annotation at each call. Default is None (built with
276
+ `build_orbit`).
277
+
278
+ Returns
279
+ -------
280
+ numpy.ndarray
281
+ Steering Doppler rate ``2 * v / lambda * az_steering_rate`` [Hz/s], repeated for each range time.
282
+ """
283
+
284
+ # Derive AR info
285
+ AR_vec = angles2freq_conversion(metadata_dict, az_t, orbit) * az_steering_rate * np.ones(len(range_times))
286
+
287
+ return AR_vec
288
+
289
+ def extract_FR_info(metadata_dict, range_times, burst_idx = None, is_grd: bool = False, subswath = None):
290
+ """
291
+ Evaluate the azimuth FM rate over the range axis.
292
+
293
+ Parameters
294
+ ----------
295
+ metadata_dict : dict
296
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
297
+ range_times : numpy.ndarray
298
+ Two-way slant range times [s] at which the FM rate polynomial is evaluated.
299
+ burst_idx : int, optional
300
+ Index of the azimuth FM rate record to use (SLC only). Default is None (first record).
301
+ is_grd : bool, optional
302
+ Whether the product is GRD. Default is False.
303
+ subswath : str, optional
304
+ Subswath identifier, required when `is_grd` is True. Default is None.
305
+
306
+ Returns
307
+ -------
308
+ FR : numpy.ndarray
309
+ Azimuth FM rate [Hz/s] at each range time.
310
+ az_mid_time : PreciseDateTime
311
+ Azimuth time of the FM rate record used (time-shifted for GRD products, see `extract_FMrate`).
312
+
313
+ Notes
314
+ -----
315
+ The first record returned by `extract_FMrate` is used. For GRD products `burst_idx` is ignored, so the first
316
+ annotated record is always used.
317
+ """
318
+
319
+ # Extract FMrate info
320
+ burst_times, FM_poly_coeffs = extract_FMrate(metadata_dict = metadata_dict,
321
+ burst_index = burst_idx,
322
+ is_grd = is_grd,
323
+ subswath = subswath)
324
+
325
+ FR = FM_poly_coeffs[0].evaluate(range_times) # burst_idx is handled inside extract_FMrate
326
+ az_mid_time = burst_times[0]
327
+
328
+ return FR, az_mid_time
329
+
330
+ def build_orbit(metadata_dict):
331
+ """
332
+ Build the orbit trajectory from the annotated orbit times, positions and velocities.
333
+
334
+ Parameters
335
+ ----------
336
+ metadata_dict : dict
337
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
338
+
339
+ Returns
340
+ -------
341
+ CubicSplineTrajectory
342
+ Orbit trajectory interpolating the annotated state vectors.
343
+ """
344
+
345
+ date_time_vec, positions, velocities = extract_info_orbita(metadata_dict)
346
+
347
+ return CubicSplineTrajectory(times=date_time_vec, positions=positions, velocities=velocities)
348
+
349
+ def angles2freq_conversion(metadata_dict, az_t, orbit = None):
350
+ """
351
+ Return the factor converting an azimuth angular rate into a Doppler rate.
352
+
353
+ Parameters
354
+ ----------
355
+ metadata_dict : dict
356
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
357
+ az_t : PreciseDateTime
358
+ Azimuth time at which the satellite velocity is evaluated.
359
+ orbit : CubicSplineTrajectory, optional
360
+ Orbit trajectory. Default is None (built with `build_orbit`).
361
+
362
+ Returns
363
+ -------
364
+ float
365
+ Conversion factor ``2 * v / lambda`` [Hz/rad], with ``v`` the satellite velocity norm at `az_t` and ``lambda``
366
+ the radar wavelength derived from ``radarFrequency``.
367
+ """
368
+
369
+ fc_hz = float(metadata_dict['product']['generalAnnotation']['productInformation']['radarFrequency'])
370
+
371
+ if orbit is None:
372
+ orbit = build_orbit(metadata_dict)
373
+ velocity = np.linalg.norm(orbit.velocity(az_t))
374
+
375
+ return 2 * velocity / (speed_of_light / fc_hz)
376
+
377
+ def convert_az_times(times):
378
+ """
379
+ Convert a sequence of UTC time strings into PreciseDateTime objects.
380
+
381
+ Parameters
382
+ ----------
383
+ times : list of str
384
+ UTC time strings, as annotated.
385
+
386
+ Returns
387
+ -------
388
+ numpy.ndarray
389
+ Array of PreciseDateTime objects.
390
+ """
391
+
392
+ date_time_vec = np.ndarray((len(times),), dtype=PreciseDateTime)
393
+ for idx, date in enumerate(times):
394
+ date_time_vec[idx] = PreciseDateTime.from_utc_string(date)
395
+
396
+ return date_time_vec
397
+
398
+ def extract_FMrate(metadata_dict, burst_index=None, is_grd: bool = False, subswath = None):
399
+ """
400
+ Read the azimuth FM rate records from the annotation.
401
+
402
+ Parameters
403
+ ----------
404
+ metadata_dict : dict
405
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
406
+ burst_index : int, optional
407
+ If given (and `is_grd` is False), only the record at this index is returned. Default is None (all records).
408
+ is_grd : bool, optional
409
+ Whether the product is GRD. Default is False.
410
+ subswath : str, optional
411
+ Subswath identifier (e.g. ``"IW2"``), required when `is_grd` is True. Default is None.
412
+
413
+ Returns
414
+ -------
415
+ burst_times_vec : numpy.ndarray
416
+ Azimuth times of the records, as PreciseDateTime objects.
417
+ polynomials : list of PolynomialCoefficients2
418
+ FM rate polynomials [Hz/s] in two-way slant range time, one per record.
419
+
420
+ Raises
421
+ ------
422
+ KeyError
423
+ If `is_grd` is True and `subswath` is not an IW/EW subswath.
424
+
425
+ Notes
426
+ -----
427
+ The ``t0`` of the first selected record is used as reference time for all polynomials. For GRD products, the record
428
+ times are shifted by ``k * (t[1] - t[0]) / n_swaths``, where ``k`` is the zero-based subswath index and ``n_swaths``
429
+ the number of swaths in the swath merge list; no shift is applied if only one record is present.
430
+ """
431
+
432
+ fmrate_list = ensure_list(metadata_dict['product']['generalAnnotation']['azimuthFmRateList']['azimuthFmRate'])
433
+ tot_swath_number = int(metadata_dict['product']['swathMerging']['swathMergeList']['@count'])
434
+
435
+ # Handle burst_index if provided
436
+ if burst_index is not None and not is_grd:
437
+ fmrate_list = [fmrate_list[burst_index]]
438
+
439
+ # Extract t0
440
+ t0 = float(fmrate_list[0]['t0'])
441
+
442
+ times = []
443
+ polynomials = []
444
+
445
+ for element in fmrate_list:
446
+ times.append(PreciseDateTime.from_utc_string(element['azimuthTime']))
447
+
448
+ poly_values = element['azimuthFmRatePolynomial']['#text'].split(' ')
449
+ coefficients = PolynomialCoefficients2(
450
+ x0=t0,
451
+ termine_noto=float(poly_values[0]),
452
+ coeff_lineare=float(poly_values[1]),
453
+ coeff_quadratico=float(poly_values[2])
454
+ )
455
+ polynomials.append(coefficients)
456
+
457
+ # Convert times to numpy array
458
+ burst_times_vec = np.array(times, dtype=object)
459
+ delta = 0
460
+
461
+ if is_grd:
462
+ swath_in_swaths = {
463
+ "IW1": 0,
464
+ "EW1": 0,
465
+ "IW2": 1,
466
+ "EW2": 1,
467
+ "IW3": 2,
468
+ "EW3": 2,
469
+ "EW4": 3,
470
+ "EW5": 4,
471
+ }
472
+
473
+ current_swath = swath_in_swaths[subswath]
474
+ if burst_times_vec.size > 1:
475
+ delta = current_swath * (burst_times_vec[1] - burst_times_vec[0]) / tot_swath_number
476
+ else:
477
+ denoise_logger.warning(f"Only one azimuth FM rate record found: no time shift applied for subswath {subswath}")
478
+
479
+ return burst_times_vec + delta, polynomials
480
+
481
+ class PolynomialCoefficients2():
482
+ """
483
+ Second-order polynomial in ``(x - x0)``.
484
+
485
+ Parameters
486
+ ----------
487
+ x0 : float
488
+ Reference abscissa.
489
+ termine_noto : float
490
+ Constant term.
491
+ coeff_lineare : float
492
+ Linear coefficient.
493
+ coeff_quadratico : float
494
+ Quadratic coefficient.
495
+
496
+ Attributes
497
+ ----------
498
+ x0 : float
499
+ Reference abscissa.
500
+ termine_noto : float
501
+ Constant term.
502
+ coeff_lineare : float
503
+ Linear coefficient.
504
+ coeff_quadratico : float
505
+ Quadratic coefficient.
506
+ """
507
+ x0: float
508
+ termine_noto: float
509
+ coeff_lineare: float
510
+ coeff_quadratico: float
511
+
512
+ def __init__(self, x0, termine_noto, coeff_lineare, coeff_quadratico):
513
+ self.x0 = x0
514
+ self.termine_noto = termine_noto
515
+ self.coeff_lineare = coeff_lineare
516
+ self.coeff_quadratico = coeff_quadratico
517
+
518
+ def evaluate(self, x):
519
+ """
520
+ Evaluate the polynomial.
521
+
522
+ Parameters
523
+ ----------
524
+ x : float or numpy.ndarray
525
+ Abscissa value(s).
526
+
527
+ Returns
528
+ -------
529
+ float or numpy.ndarray
530
+ Polynomial value(s) at `x`.
531
+ """
532
+ return self.termine_noto + self.coeff_lineare * (x - self.x0) + self.coeff_quadratico * ((x - self.x0) **2)
533
+
534
+
535
+ def extract_info_orbita(metadata_dict):
536
+ """
537
+ Read the annotated orbit state vectors.
538
+
539
+ Parameters
540
+ ----------
541
+ metadata_dict : dict
542
+ Annotation dictionary, as returned by `read_metadata_from_safe_annotation`.
543
+
544
+ Returns
545
+ -------
546
+ times : numpy.ndarray
547
+ Orbit state vector times, as PreciseDateTime objects, shape ``(n,)``.
548
+ positions : numpy.ndarray
549
+ Positions [m], shape ``(n, 3)``.
550
+ velocities : numpy.ndarray
551
+ Velocities [m/s], shape ``(n, 3)``.
552
+ """
553
+
554
+ data_orbita = metadata_dict['product']['generalAnnotation']['orbitList']['orbit']
555
+
556
+ times, x, y, z, vx, vy, vz = [], [], [], [], [], [], []
557
+ for element in data_orbita:
558
+ times.append(element['time'])
559
+ x.append(float(element['position']['x']))
560
+ y.append(float(element['position']['y']))
561
+ z.append(float(element['position']['z']))
562
+ vx.append(float(element['velocity']['x']))
563
+ vy.append(float(element['velocity']['y']))
564
+ vz.append(float(element['velocity']['z']))
565
+
566
+ # Convert times
567
+ date_time_vec = np.ndarray((len(times),), dtype=PreciseDateTime)
568
+ for idx, date in enumerate(times):
569
+ date_time_vec[idx] = PreciseDateTime.from_utc_string(date)
570
+
571
+ return np.array(date_time_vec), np.c_[x, y, z], np.c_[vx, vy, vz]