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/utils.py ADDED
@@ -0,0 +1,282 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ General-purpose utilities for the denoising pipeline.
6
+
7
+ Provides helpers for linear/dB and ISO timestamp conversions, swath range boundary lookup, slant-to-ground and
8
+ ground-to-slant range conversions, outlier rejection of noise pulses in noise power series, and list normalisation.
9
+ """
10
+
11
+ import numpy as np
12
+
13
+ from perseo_core.timing import PreciseDateTime
14
+
15
+ from s1_denoise import NOISE_LOWER_BOUND, NOISE_UPPER_BOUND
16
+
17
+
18
+ def linear_to_db(value, is_amplitude=False):
19
+ """
20
+ Convert values from linear scale to dB.
21
+
22
+ Parameters
23
+ ----------
24
+ value : float or array_like
25
+ Value(s) in linear scale.
26
+ is_amplitude : bool, optional
27
+ If True, `value` is an amplitude and ``20 * log10`` is used, otherwise ``10 * log10`` (power). Default is False.
28
+
29
+ Returns
30
+ -------
31
+ float or numpy.ndarray
32
+ Value(s) in dB; a float for scalar input.
33
+ """
34
+
35
+ factor = 20 if is_amplitude else 10
36
+ db = factor * np.log10(np.asarray(value, dtype=float))
37
+
38
+ return float(db) if db.ndim == 0 else db
39
+
40
+ def db_to_linear(value, is_amplitude=False):
41
+ """
42
+ Convert values from dB to linear scale.
43
+
44
+ Parameters
45
+ ----------
46
+ value : float or array_like
47
+ Value(s) in dB.
48
+ is_amplitude : bool, optional
49
+ If True, an amplitude is returned (``10 ** (x / 20)``), otherwise a power (``10 ** (x / 10)``). Default is False.
50
+
51
+ Returns
52
+ -------
53
+ float or numpy.ndarray
54
+ Value(s) in linear scale; a float for scalar input.
55
+ """
56
+
57
+ factor = 20 if is_amplitude else 10
58
+ linear = 10 ** (np.asarray(value, dtype=float) / factor)
59
+
60
+ return float(linear) if linear.ndim == 0 else linear
61
+
62
+ def iso_to_datetime(iso_str: str) -> PreciseDateTime:
63
+ """
64
+ Convert an ISO timestamp string to a PreciseDateTime object.
65
+
66
+ Parameters
67
+ ----------
68
+ iso_str : str
69
+ ISO 8601 timestamp.
70
+
71
+ Returns
72
+ -------
73
+ PreciseDateTime
74
+ Corresponding time.
75
+ """
76
+ return PreciseDateTime.fromisoformat(iso_str)
77
+
78
+ def get_range_boundaries(swath_merges, subswath, az_time: str | PreciseDateTime):
79
+ """
80
+ Return the first and last GRD range samples of a subswath at a given azimuth time.
81
+
82
+ The swath bounds record used is the latest one with azimuth time not after `az_time`, or the first record if none
83
+ precedes it.
84
+
85
+ Parameters
86
+ ----------
87
+ swath_merges : list of dict
88
+ ``swathMerge`` records of the annotation.
89
+ subswath : str
90
+ Subswath identifier (e.g. ``"IW2"``).
91
+ az_time : str or PreciseDateTime
92
+ Azimuth time (UTC string or PreciseDateTime).
93
+
94
+ Returns
95
+ -------
96
+ first_range_sample : int
97
+ First range sample of the subswath.
98
+ last_range_sample : int
99
+ Last range sample of the subswath.
100
+
101
+ Raises
102
+ ------
103
+ TypeError
104
+ If `az_time` is neither a str nor a PreciseDateTime.
105
+ """
106
+
107
+ if isinstance(az_time, str):
108
+ az_time = PreciseDateTime.from_utc_string(az_time)
109
+ elif not isinstance(az_time, PreciseDateTime):
110
+ raise TypeError("az_time must be str or PreciseDateTime")
111
+
112
+ for swath_merge in swath_merges:
113
+
114
+ if swath_merge["swath"] == subswath:
115
+
116
+ swath_bounds = ensure_list(swath_merge["swathBoundsList"]["swathBounds"])
117
+
118
+ closest_swath_merge = max(
119
+ (k for k in swath_bounds
120
+ if PreciseDateTime.fromisoformat(k["azimuthTime"]) <= az_time),
121
+ key=lambda k: PreciseDateTime.fromisoformat(k["azimuthTime"]),
122
+ default=swath_bounds[0]
123
+ )
124
+
125
+ first_range_sample = int(closest_swath_merge["firstRangeSample"])
126
+ last_range_sample = int(closest_swath_merge["lastRangeSample"])
127
+
128
+ return first_range_sample, last_range_sample
129
+
130
+ def ground2slant(start_az_time, gr_array, coordinate_conversions):
131
+ """
132
+ Convert ground range distances to slant range distances.
133
+
134
+ The GRSR polynomial of the coordinate conversion record closest in azimuth to `start_az_time` is evaluated in
135
+ ``(gr - gr0)``.
136
+
137
+ Parameters
138
+ ----------
139
+ start_az_time : str
140
+ ISO azimuth time used to select the coordinate conversion record.
141
+ gr_array : array_like
142
+ Ground range distances [m].
143
+ coordinate_conversions : list of dict
144
+ ``coordinateConversion`` records of the annotation (with ``azimuthTime``, ``gr0`` and ``grsrCoefficients``).
145
+
146
+ Returns
147
+ -------
148
+ numpy.ndarray
149
+ Slant range distances [m].
150
+ """
151
+ gr_array = np.asarray(gr_array, dtype=float)
152
+
153
+ # select closest GRSR polynomial in azimuth
154
+ cc = min(
155
+ coordinate_conversions,
156
+ key=lambda cc: abs(iso_to_datetime(cc["azimuthTime"]) - iso_to_datetime(start_az_time))
157
+ )
158
+
159
+ sr0 = float(cc["sr0"])
160
+ gr0 = float(cc["gr0"])
161
+ grsr_coeffs = [float(c) for c in cc["grsrCoefficients"]['#text'].split(' ')]
162
+
163
+ delta_gr = gr_array - gr0
164
+ sr_array = np.polyval(grsr_coeffs[::-1], delta_gr) # np.polyval usa coeff in ordine decrescente
165
+
166
+ return sr_array
167
+
168
+ def slant2ground(start_az_time, sim_noise_sr, inc_angle_sr, coordinate_conversions, sr_distances):
169
+ """
170
+ Resample slant range data onto a uniform ground range grid.
171
+
172
+ The slant range distances are converted to ground range with the SRGR polynomial of the coordinate conversion record
173
+ closest in azimuth to `start_az_time`, as ``gr0 + P(sr - sr0)``. Both inputs are then linearly interpolated onto a
174
+ uniform ground range grid spanning the converted distances, with the same number of samples (NaN outside).
175
+
176
+ Parameters
177
+ ----------
178
+ start_az_time : str
179
+ ISO azimuth time used to select the coordinate conversion record.
180
+ sim_noise_sr : array_like
181
+ Simulated noise on the slant range grid.
182
+ inc_angle_sr : array_like
183
+ Incidence-angle-related values on the slant range grid (the GRD pipeline passes their sine).
184
+ coordinate_conversions : list of dict
185
+ Coordinate conversion records with ``azimuthTime``, ``sr0``, ``gr0`` and ``srgrCoeffs`` (list of float).
186
+ sr_distances : array_like
187
+ Slant range distances [m] of the samples of `sim_noise_sr` and `inc_angle_sr`.
188
+
189
+ Returns
190
+ -------
191
+ sim_noise_gr : numpy.ndarray
192
+ Simulated noise on the uniform ground range grid.
193
+ inc_angle_gr : numpy.ndarray
194
+ `inc_angle_sr` resampled on the uniform ground range grid.
195
+ """
196
+ sim_noise_sr = np.asarray(sim_noise_sr, dtype=float)
197
+ inc_angle_sr = np.asarray(inc_angle_sr, dtype=float)
198
+ sr_distances = np.asarray(sr_distances, dtype=float)
199
+
200
+ # select closest SRGR polynomial in azimuth
201
+ cc = min(
202
+ coordinate_conversions,
203
+ key=lambda cc: abs(iso_to_datetime(cc["azimuthTime"]) - iso_to_datetime(start_az_time))
204
+ )
205
+
206
+ sr0 = float(cc["sr0"])
207
+ gr0 = float(cc["gr0"])
208
+ sr_to_gr_polynomial = cc["srgrCoeffs"]
209
+
210
+ # from SR distances to GR distances (metres)
211
+ sr_norm = sr_distances - sr0
212
+ gr = gr0 + np.polyval(sr_to_gr_polynomial[::-1], sr_norm)
213
+
214
+ # resample onto a uniform GR grid of same length
215
+ gr_grid = np.linspace(gr.min(), gr.max(), sim_noise_sr.size)
216
+
217
+ sim_noise_gr = np.interp(gr_grid, gr, sim_noise_sr, left=np.nan, right=np.nan)
218
+ inc_angle_gr = np.interp(gr_grid, gr, inc_angle_sr, left=np.nan, right=np.nan)
219
+
220
+ return sim_noise_gr, inc_angle_gr
221
+
222
+ def remove_outliers(azimuth_times, noise_pulses, expected_noise_value):
223
+ """
224
+ Remove the noise pulses falling outside an interval around the expected noise power, with their azimuth times.
225
+
226
+ A pulse is retained if it lies within ``[P_exp - NOISE_LOWER_BOUND dB, P_exp + NOISE_UPPER_BOUND dB]``, with
227
+ ``P_exp`` the expected noise power.
228
+
229
+ Parameters
230
+ ----------
231
+ azimuth_times : array_like
232
+ Azimuth times of the noise pulses, same length as `noise_pulses`.
233
+ noise_pulses : array_like
234
+ Noise power of each pulse (linear scale).
235
+ expected_noise_value : float
236
+ Expected noise power from AUX_INS (linear scale).
237
+
238
+ Returns
239
+ -------
240
+ azimuth_times_clean : numpy.ndarray
241
+ Azimuth times of the retained pulses.
242
+ noise_pulses_clean : numpy.ndarray
243
+ Noise power of the retained pulses.
244
+ outlier_indices : list of list
245
+ Removed pulses, as ``[index, azimuth_time, noise_pulse]``, sorted by index.
246
+ """
247
+
248
+ azimuth_times = np.array(azimuth_times)
249
+ noise_pulses = np.array(noise_pulses, dtype=float)
250
+ min_value, max_value = expected_noise_value * db_to_linear(-NOISE_LOWER_BOUND), expected_noise_value * db_to_linear(NOISE_UPPER_BOUND)
251
+
252
+ # Detect outliers
253
+ mask = (noise_pulses < min_value) | (noise_pulses > max_value)
254
+ indices = np.where(mask)[0]
255
+
256
+ # Collect outlier info (already sorted by original index)
257
+ outlier_indices = [[idx, azimuth_times[idx], noise_pulses[idx]] for idx in indices]
258
+
259
+ # Remove outliers
260
+ azimuth_times_clean = azimuth_times[~mask]
261
+ noise_pulses_clean = noise_pulses[~mask]
262
+
263
+ return azimuth_times_clean, noise_pulses_clean, outlier_indices
264
+
265
+ def ensure_list(x) -> list:
266
+ """
267
+ Return the input as a list.
268
+
269
+ If `x` is already a list it is returned unchanged, otherwise it is wrapped in a single-element list.
270
+
271
+ Parameters
272
+ ----------
273
+ x : object
274
+ Input value.
275
+
276
+ Returns
277
+ -------
278
+ list
279
+ `x` itself, or ``[x]``.
280
+ """
281
+
282
+ return x if isinstance(x, list) else [x]