sct-cosmo-reader 1.0.0__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,6 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """SCT-Plugin: COSMO-SkyMed product format reader."""
5
+
6
+ __version__ = "1.0.0"
@@ -0,0 +1,36 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Cosmo-SkyMed SCT plugin interface."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Callable
10
+
11
+ from sct_cosmo_reader import __version__
12
+
13
+ if TYPE_CHECKING:
14
+ from sct.io.extended_protocols import ALECorrectionFunctionType, SCTInputProduct
15
+
16
+
17
+ class COSMOProductPlugin:
18
+ """Plugin for COSMO product format"""
19
+
20
+ version = __version__
21
+
22
+ @classmethod
23
+ def get_manager(cls) -> type[SCTInputProduct]:
24
+ from sct_cosmo_reader.protocol_implementation import COSMOProductManager
25
+
26
+ return COSMOProductManager
27
+
28
+ @classmethod
29
+ def get_detector(cls) -> Callable[[str | Path], bool]:
30
+ from eo_products.cosmo.utilities import is_cosmo_product
31
+
32
+ return is_cosmo_product
33
+
34
+ @classmethod
35
+ def get_ale_corrector(cls) -> ALECorrectionFunctionType:
36
+ return None
@@ -0,0 +1,813 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Cosmo-SkyMed format reader protocol-compliant wrapper for PERSEO-quality."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from itertools import product
9
+ from pathlib import Path
10
+
11
+ import numpy as np
12
+ from eo_products.common.utilities import DopplerEvaluator
13
+ from eo_products.cosmo.reader import open_product, read_channel_data, read_channel_metadata
14
+ from numpy.typing import ArrayLike
15
+ from perseo_core.geometry import compute_ground_velocity, compute_incidence_angles, compute_look_angles
16
+ from perseo_core.geometry.geocoding import inverse_geocoding_monostatic
17
+ from perseo_core.geometry.navigation import Trajectory
18
+ from perseo_core.timing import PreciseDateTime
19
+ from perseo_quality.core.custom_errors import (
20
+ CoordinatesOutOfBounds,
21
+ )
22
+ from perseo_quality.core.generic_dataclasses import (
23
+ LocationData,
24
+ SARAcquisitionMode,
25
+ SARImageType,
26
+ SAROrbitDirection,
27
+ SARPolarization,
28
+ SARProjection,
29
+ SARRadiometricQuantity,
30
+ SARSamplingFrequencies,
31
+ SARSideLooking,
32
+ )
33
+ from perseo_quality.core.signal_processing import radiometric_correction
34
+ from perseo_quality.io.protocol_utilities import roi_validation
35
+ from scipy.constants import speed_of_light
36
+ from shapely import Polygon
37
+
38
+
39
+ class COSMODopplerPolynomial:
40
+ """PERSEO-quality Doppler Function protocol compliant COSMO doppler polynomial wrapper"""
41
+
42
+ def __init__(self, evaluator: DopplerEvaluator) -> None:
43
+ self._evaluator = evaluator
44
+
45
+ def evaluate(self, azimuth_time: PreciseDateTime, range_time: float) -> float:
46
+ """Evaluate the Doppler Polynomial at given azimuth and range times.
47
+
48
+ Parameters
49
+ ----------
50
+ azimuth_time : PreciseDateTime
51
+ azimuth time at which evaluate the polynomial
52
+ range_time : float
53
+ range time at which evaluate the polynomial
54
+
55
+ Returns
56
+ -------
57
+ float
58
+ doppler centroid at that time
59
+ """
60
+ return self._evaluator.evaluate(azimuth_time, range_time)
61
+
62
+
63
+ class COSMOProductManager:
64
+ """SCTInputProduct protocol compliant COSMO wrapper"""
65
+
66
+ def __init__(self, path: str | Path, **kwargs) -> None:
67
+ self._path = Path(path)
68
+ self._name = self._path.name
69
+ self._product = open_product(path)
70
+ region_corners = list(product(self._product.footprint[:2], self._product.footprint[2:]))
71
+ self._footprint = Polygon(region_corners).convex_hull
72
+
73
+ @property
74
+ def path(self) -> Path:
75
+ """Get product path"""
76
+ return self._path
77
+
78
+ @property
79
+ def name(self) -> str:
80
+ """Get product name"""
81
+ return self._name
82
+
83
+ @property
84
+ def footprint(self) -> Polygon:
85
+ """Get product footprint"""
86
+ return self._footprint
87
+
88
+ @property
89
+ def channels_list(self) -> list[str]:
90
+ """Get list of available channels for this product"""
91
+ return self._product.channels_list
92
+
93
+ def get_channel_data(self, channel_id: str) -> COSMOChannelManager:
94
+ """Gathering all the information that are channel dependent and storing them in a protocol compliant object.
95
+
96
+ Parameters
97
+ ----------
98
+ channel_id : int
99
+ selected channel identifier
100
+
101
+ Returns
102
+ -------
103
+ COSMOChannelManager
104
+ ChannelData-compliant object containing data corresponding to the selected channel
105
+ """
106
+ return COSMOChannelManager(
107
+ product_path=self.path,
108
+ channel_name=channel_id,
109
+ )
110
+
111
+
112
+ class COSMOChannelManager:
113
+ """PERSEO-quality ChannelData protocol compliant COSMO channel wrapper"""
114
+
115
+ def __init__(
116
+ self,
117
+ product_path: Path,
118
+ channel_name: str,
119
+ ) -> None:
120
+ """Creating a ChannelManager object compliant with the ChannelData protocol.
121
+
122
+ Parameters
123
+ ----------
124
+ product_path : int
125
+ Path to the product
126
+ channel_name : int
127
+ name of current channel
128
+ """
129
+
130
+ self._channel_id = channel_name
131
+ self._raster_file = product_path
132
+ self._channel = read_channel_metadata(file_path=product_path, channel_id=channel_name)
133
+ self._sensor_name = (
134
+ "" if self._channel.dataset_info.sensor_name is None else self._channel.dataset_info.sensor_name
135
+ )
136
+
137
+ self._radiometric_quantity = SARRadiometricQuantity[self._channel.image_radiometric_quantity.name]
138
+ self._polarization = SARPolarization(self._channel.general_info.polarization.value)
139
+ self._projection = SARProjection(self._channel.general_info.projection.value)
140
+ self._orbit_direction = SAROrbitDirection[self._channel.general_info.orbit_direction.name]
141
+ self._acquisition_mode = SARAcquisitionMode[self._channel.general_info.acquisition_mode_std.name]
142
+ self._looking_side = SARSideLooking(self._channel.dataset_info.side_looking)
143
+
144
+ self._range_step_m = self._compute_range_step_m()
145
+ self._image_type = self._channel.general_info.product_level
146
+
147
+ # compute axes
148
+ self._azimuth_axis = self._compute_azimuth_axis()
149
+ self._az_time_half_swath = self._azimuth_axis[self._azimuth_axis.size // 2]
150
+ self._range_axis = (
151
+ np.arange(0, self._channel.raster_info.samples.length, 1) * self._channel.raster_info.samples.step
152
+ + self._channel.raster_info.samples.start
153
+ )
154
+ self._slant_range_axis = self._compute_slant_range_axis()
155
+ rng_time_half_swath = (
156
+ self._channel.raster_info.samples.start
157
+ + (self._channel.raster_info.samples.length - 1) * self._channel.raster_info.samples.step / 2
158
+ )
159
+ if self._projection == SARProjection.GROUND_RANGE:
160
+ rng_time_half_swath = self._channel.coordinate_conversions.evaluate_ground_to_slant(
161
+ azimuth_time=self._az_time_half_swath, ground_range=np.floor(rng_time_half_swath)
162
+ )
163
+ self._rng_time_half_swath = rng_time_half_swath
164
+
165
+ # lines per burst array
166
+ if self._channel.burst_info.num > 0:
167
+ self._lines_per_burst_array = np.repeat(
168
+ self._channel.burst_info.lines_per_burst, self._channel.burst_info.num
169
+ )
170
+ else:
171
+ # should be a 1D array
172
+ self._lines_per_burst_array = np.repeat(self._channel.raster_info.lines.length, 1)
173
+
174
+ # prf
175
+ self._prf = self._channel.swath_info.prf
176
+
177
+ # steering rate
178
+ self._steering_rate_poly_coeff = self._channel.swath_info.azimuth_steering_rate_poly
179
+
180
+ # generating trajectory from orbit
181
+ self._trajectory_rx = self._channel.orbit
182
+ self._trajectory_tx = None
183
+
184
+ # generating doppler centroid wrappers
185
+ self._doppler_centroid_poly = COSMODopplerPolynomial(evaluator=self._channel.doppler_centroid_poly)
186
+
187
+ # get burst boundaries
188
+ self._burst_az_boundaries, self._burst_rng_boundaries = self._get_raster_layout()
189
+
190
+ def _compute_range_step_m(self) -> float:
191
+ """Computing step along range direction, in meters"""
192
+ if self._projection == SARProjection.GROUND_RANGE:
193
+ return self._channel.raster_info.samples.step
194
+ return self._channel.raster_info.samples.step * speed_of_light / 2
195
+
196
+ def _compute_slant_range_axis(self) -> np.ndarray:
197
+ """Computing slant range full axis.
198
+
199
+ Returns
200
+ -------
201
+ np.ndarray
202
+ slant range axis
203
+ """
204
+ slant_rng_axis = self._range_axis
205
+ if self._projection == SARProjection.GROUND_RANGE:
206
+ slant_rng_axis = self._channel.coordinate_conversions.evaluate_ground_to_slant(
207
+ azimuth_time=self._az_time_half_swath, ground_range=self._range_axis
208
+ )
209
+ return slant_rng_axis
210
+
211
+ def _compute_azimuth_axis(self) -> np.ndarray:
212
+ """Compute azimuth full axis.
213
+
214
+ Returns
215
+ -------
216
+ np.ndarray
217
+ azimuth axis
218
+ """
219
+ az_axis = (
220
+ np.arange(0, self._channel.raster_info.lines.length, 1) * self._channel.raster_info.lines.step
221
+ + self._channel.raster_info.lines.start
222
+ )
223
+ if self._channel.burst_info.num > 0:
224
+ az_axis = []
225
+ for brst in range(self._channel.burst_info.num):
226
+ az_axis.append(
227
+ self._channel.burst_info.azimuth_start_times[brst]
228
+ + np.arange(0, self._channel.burst_info.lines_per_burst, 1) * self._channel.raster_info.lines.step
229
+ )
230
+ az_axis = np.concatenate(az_axis)
231
+ return az_axis
232
+
233
+ def _get_raster_layout(self) -> tuple[list[PreciseDateTime], list[float]]:
234
+ """Evaluating raster boundaries taking into account the bursts, if needed.
235
+
236
+ Returns
237
+ -------
238
+ tuple[list[list[PreciseDateTime, PreciseDateTime]], list[list[float, float]]]
239
+ azimuth raster boundaries (azimuth start, azimuth stop),
240
+ range raster boundaries (range start, range stop)
241
+ """
242
+
243
+ if self._channel.burst_info.num > 0:
244
+ az_times = self._channel.burst_info.azimuth_start_times
245
+ rng_times = np.repeat(self._channel.raster_info.samples.start, az_times.size)
246
+ burst_az_boundaries = []
247
+ for az_time in az_times:
248
+ burst_az_boundaries.append(
249
+ [az_time, az_time + self._channel.burst_info.lines_per_burst * self._channel.raster_info.lines.step]
250
+ )
251
+ burst_rng_boundaries = []
252
+ for rng_time in rng_times:
253
+ burst_rng_boundaries.append(
254
+ [
255
+ rng_time,
256
+ rng_time + self._channel.raster_info.samples.length * self._channel.raster_info.samples.step,
257
+ ]
258
+ )
259
+ else:
260
+ burst_az_boundaries = [
261
+ [
262
+ self._channel.raster_info.lines.start,
263
+ self._channel.raster_info.lines.start
264
+ + self._channel.raster_info.lines.length * self._channel.raster_info.lines.step,
265
+ ]
266
+ ]
267
+ burst_rng_boundaries = [
268
+ [
269
+ self._channel.raster_info.samples.start,
270
+ self._channel.raster_info.samples.start
271
+ + self._channel.raster_info.samples.length * self._channel.raster_info.samples.step,
272
+ ]
273
+ ]
274
+
275
+ return burst_az_boundaries, burst_rng_boundaries
276
+
277
+ @property
278
+ def sensor_name(self) -> str:
279
+ """Name of the sensor"""
280
+ return self._sensor_name
281
+
282
+ @property
283
+ def swath_name(self) -> str:
284
+ """Name of the swath being analyzed"""
285
+ return self._channel.general_info.swath
286
+
287
+ @property
288
+ def channel_id(self) -> str:
289
+ """Identifier of current channel data"""
290
+ return self._channel_id
291
+
292
+ @property
293
+ def prf(self) -> float:
294
+ """Sensor Pulse Repetition Frequency (PRF)"""
295
+ return self._prf
296
+
297
+ @property
298
+ def range_step_m(self) -> float:
299
+ """Step along range direction, in meters"""
300
+ return self._range_step_m
301
+
302
+ @property
303
+ def azimuth_step_s(self) -> float:
304
+ """Step along azimuth direction, in seconds"""
305
+ return self._channel.raster_info.lines.step
306
+
307
+ @property
308
+ def projection(self) -> SARProjection:
309
+ """Channel data projection"""
310
+ return self._projection
311
+
312
+ @property
313
+ def polarization(self) -> SARPolarization:
314
+ """Channel data polarization"""
315
+ return self._polarization
316
+
317
+ @property
318
+ def acquisition_mode(self) -> SARAcquisitionMode:
319
+ """Channel data acquisition mode"""
320
+ return self._acquisition_mode
321
+
322
+ @property
323
+ def orbit_direction(self) -> SAROrbitDirection:
324
+ """Channel data orbit direction"""
325
+ return self._orbit_direction
326
+
327
+ @property
328
+ def image_type(self) -> SARImageType:
329
+ """Channel raster image type"""
330
+ return self._image_type
331
+
332
+ @property
333
+ def sampling_constants(self) -> SARSamplingFrequencies:
334
+ """Channel data signal sampling frequencies"""
335
+ return self._channel.sampling_constants
336
+
337
+ @property
338
+ def looking_side(self) -> SARSideLooking:
339
+ """Sensor look direction for this channel"""
340
+ return self._looking_side
341
+
342
+ @property
343
+ def carrier_frequency(self) -> float:
344
+ """Signal carrier frequency"""
345
+ return self._channel.dataset_info.fc_hz
346
+
347
+ @property
348
+ def mid_azimuth_time(self) -> PreciseDateTime:
349
+ """Azimuth time at half swath"""
350
+ return self._az_time_half_swath
351
+
352
+ @property
353
+ def trajectory(self) -> Trajectory:
354
+ """Channel trajectory rx 3D curve"""
355
+ return self._trajectory_rx
356
+
357
+ @property
358
+ def attitude(self) -> None:
359
+ """Channel attitude defined in ECEF Reference Frame"""
360
+ return None
361
+
362
+ @property
363
+ def doppler_centroid(self) -> COSMODopplerPolynomial:
364
+ """Channel doppler centroid polynomial wrapper"""
365
+ return self._doppler_centroid_poly
366
+
367
+ @property
368
+ def doppler_rate(self) -> None:
369
+ """Channel doppler rate polynomial wrapper"""
370
+ return None
371
+
372
+ @property
373
+ def mid_range_time(self) -> float:
374
+ """Range time at half swath"""
375
+ return self._rng_time_half_swath
376
+
377
+ @property
378
+ def range_axis(self) -> np.ndarray:
379
+ """Range axis"""
380
+ return self._range_axis
381
+
382
+ @property
383
+ def slant_range_axis(self) -> np.ndarray:
384
+ """Range axis"""
385
+ return self._slant_range_axis
386
+
387
+ @property
388
+ def azimuth_axis(self) -> np.ndarray:
389
+ """Azimuth axis, PreciseDateTime format"""
390
+ return self._azimuth_axis
391
+
392
+ @property
393
+ def lines_per_burst(self) -> np.ndarray:
394
+ """Lines per burst, for each burst in the swath"""
395
+ return self._lines_per_burst_array
396
+
397
+ @property
398
+ def radiometric_quantity(self) -> np.ndarray:
399
+ """Product radiometric quantity"""
400
+ return self._radiometric_quantity
401
+
402
+ def get_mid_burst_times(self, burst: int) -> tuple[PreciseDateTime, float]:
403
+ """Compute mid azimuth and range times for a given burst.
404
+
405
+ Returns
406
+ -------
407
+ PreciseDateTime
408
+ azimuth mid burst time
409
+ float
410
+ range mid burst time
411
+ """
412
+ az_mid_burst = self.mid_azimuth_time
413
+ rng_mid_burst = self.mid_range_time
414
+ if self._channel.burst_info.num > 0:
415
+ az_time_boundaries, rng_time_boundaries = self._get_raster_layout()
416
+ az_mid_burst = (az_time_boundaries[burst][1] - az_time_boundaries[burst][0]) / 2 + az_time_boundaries[
417
+ burst
418
+ ][0]
419
+ rng_mid_burst = (rng_time_boundaries[burst][1] - rng_time_boundaries[burst][0]) / 2 + rng_time_boundaries[
420
+ burst
421
+ ][0]
422
+
423
+ return az_mid_burst, rng_mid_burst
424
+
425
+ def get_steering_rate(self, azimuth_time: PreciseDateTime, burst: int) -> float:
426
+ """Compute steering rate at a given azimuth time and for a given burst.
427
+
428
+ Parameters
429
+ ----------
430
+ azimuth_time : PreciseDateTime
431
+ azimuth time
432
+ burst : int
433
+ burst corresponding to the input time
434
+
435
+ Returns
436
+ -------
437
+ float
438
+ azimuth steering rate
439
+ """
440
+ if self._channel.burst_info.num > 0 and burst is not None:
441
+ time_rel = azimuth_time - self._channel.burst_info.azimuth_start_times[burst]
442
+ else:
443
+ time_rel = azimuth_time - self._channel.raster_info.lines.start
444
+ return (
445
+ self._steering_rate_poly_coeff[0]
446
+ + self._steering_rate_poly_coeff[1] * time_rel
447
+ + self._steering_rate_poly_coeff[2] * time_rel**2
448
+ )
449
+
450
+ def get_location_data(self, azimuth_time: PreciseDateTime, range_time: float) -> LocationData:
451
+ """Generating a LocationData object containing data and info derived from the current COSMOChannelManager
452
+ and declined to the specific azimuth and range times selected.
453
+
454
+ Parameters
455
+ ----------
456
+ azimuth_time : PreciseDateTime
457
+ selected azimuth time
458
+ range_time : float
459
+ selected range time
460
+
461
+ Returns
462
+ -------
463
+ LocationData
464
+ LocationData instance related to the selected location
465
+ """
466
+
467
+ incidence_angle = compute_incidence_angles(
468
+ trajectory=self.trajectory,
469
+ azimuth_time=azimuth_time,
470
+ range_times=range_time,
471
+ look_direction=self.looking_side.value,
472
+ )
473
+ look_angle = compute_look_angles(
474
+ trajectory=self.trajectory,
475
+ azimuth_time=azimuth_time,
476
+ range_times=self.mid_range_time,
477
+ look_direction=self.looking_side.value,
478
+ )
479
+ v_ground = compute_ground_velocity(
480
+ trajectory=self.trajectory, azimuth_time=azimuth_time, look_angles_rad=look_angle
481
+ )
482
+ azimuth_step_m = self.azimuth_step_s * v_ground
483
+
484
+ # TODO: this should be a common feature, also: slant_range_step_m, not range_step_m
485
+ if self.projection == SARProjection.SLANT_RANGE:
486
+ ground_range_step_m: float = self.range_step_m / np.sin(incidence_angle)
487
+ range_step_m = self.range_step_m
488
+ elif self.projection == SARProjection.GROUND_RANGE:
489
+ ground_range_step_m: float = self.range_step_m
490
+ range_step_m = self.range_step_m * np.sin(incidence_angle)
491
+
492
+ return LocationData(
493
+ abs_azimuth_time=azimuth_time,
494
+ abs_range_time=range_time,
495
+ incidence_angle=incidence_angle,
496
+ look_angle=look_angle,
497
+ ground_velocity=v_ground,
498
+ azimuth_step_m=azimuth_step_m,
499
+ range_step_m=range_step_m,
500
+ ground_range_step_m=ground_range_step_m,
501
+ )
502
+
503
+ def pixel_to_times_conversion(
504
+ self, azimuth_index: float, range_index: float, burst: int = None
505
+ ) -> tuple[PreciseDateTime, float]:
506
+ """Converting input raster pixel coordinates (azimuth_index and range index) to corresponding absolute times,
507
+ azimuth and range.
508
+
509
+ Parameters
510
+ ----------
511
+ azimuth_index : float
512
+ azimuth pixel index, subpixel precision
513
+ range_index : float
514
+ range pixel index, subpixel precision
515
+ burst : int, optional
516
+ burst index, by default None
517
+
518
+ Returns
519
+ -------
520
+ PreciseDateTime
521
+ azimuth time
522
+ float
523
+ range time
524
+ """
525
+
526
+ start_time_rng = self._channel.raster_info.samples.start
527
+ if self._channel.burst_info.num > 0 and burst is not None:
528
+ start_time_az = self._channel.burst_info.azimuth_start_times[burst]
529
+ az_time = (
530
+ azimuth_index - self._channel.burst_info.lines_per_burst * burst
531
+ ) * self._channel.raster_info.lines.step + start_time_az
532
+ else:
533
+ start_time_az = self._channel.raster_info.lines.start
534
+ az_time = azimuth_index * self._channel.raster_info.lines.step + start_time_az
535
+
536
+ rng_time = range_index * self._channel.raster_info.samples.step + start_time_rng
537
+
538
+ if self.projection == SARProjection.GROUND_RANGE:
539
+ rng_time = self._channel.coordinate_conversions.evaluate_ground_to_slant(
540
+ azimuth_time=self.mid_azimuth_time, ground_range=rng_time
541
+ )
542
+
543
+ return az_time, rng_time
544
+
545
+ def times_to_pixel_conversion(
546
+ self, azimuth_time: PreciseDateTime, range_time: float, burst: int | None = None
547
+ ) -> tuple[float, float]:
548
+ """Converting azimuth and range times to raster image pixels indexes with subpixel precision.
549
+
550
+ Parameters
551
+ ----------
552
+ azimuth_time : PreciseDateTime
553
+ azimuth time
554
+ range_time : float
555
+ range time
556
+ burst : int | None, optional
557
+ burst number corresponding to these times, by default None
558
+
559
+ Returns
560
+ -------
561
+ float
562
+ pixel corresponding to azimuth time
563
+ float
564
+ pixel corresponding to range time
565
+ """
566
+
567
+ rng_value = range_time
568
+ if self.projection == SARProjection.GROUND_RANGE:
569
+ # if projection is GROUND RANGE, range info are expressed in meters, so it must be converted
570
+ rng_value = self._channel.coordinate_conversions.evaluate_slant_to_ground(
571
+ azimuth_time=azimuth_time, slant_range=range_time
572
+ )
573
+
574
+ rng_idx = (rng_value - self._channel.raster_info.samples.start) / self._channel.raster_info.samples.step
575
+ if self._channel.burst_info.num > 0:
576
+ if burst is None:
577
+ burst = self.times_to_burst_association([azimuth_time])[0]
578
+ azmth_idx = (
579
+ azimuth_time - self._channel.burst_info.azimuth_start_times[burst]
580
+ ) / self._channel.raster_info.lines.step + self._channel.burst_info.lines_per_burst * burst
581
+ else:
582
+ azmth_idx = (azimuth_time - self._channel.raster_info.lines.start) / self._channel.raster_info.lines.step
583
+
584
+ return azmth_idx, rng_idx
585
+
586
+ def ground_points_to_burst_association(self, coordinates: ArrayLike) -> list[list[int] | None]:
587
+ """Determining the burst (or bursts) where the input coordinates lie. If no association can be found (i.e. the
588
+ point is not visible in the scene), None is returned.
589
+
590
+ Parameters
591
+ ----------
592
+ coordinates : ArrayLike
593
+ array of coordinates, in the form (N, 3)
594
+
595
+ Returns
596
+ -------
597
+ list[list[int] | None]
598
+ list containing the burst association for each input point, None if no association was found
599
+ """
600
+
601
+ coordinates = np.atleast_2d(coordinates)
602
+
603
+ t_azmth, t_rng = [], []
604
+ for coord in coordinates:
605
+ try:
606
+ t_azmth_i, t_rng_i = inverse_geocoding_monostatic(
607
+ trajectory=self.trajectory,
608
+ ground_points=coord,
609
+ doppler_frequencies=0,
610
+ wavelength=1,
611
+ az_initial_time_guesses=self.mid_azimuth_time,
612
+ )
613
+ t_azmth.append(t_azmth_i)
614
+ t_rng.append(t_rng_i)
615
+ except Exception:
616
+ t_azmth.append(np.nan)
617
+ t_rng.append(np.nan)
618
+
619
+ t_azmth = np.asarray(t_azmth)
620
+ t_rng = np.asarray(t_rng)
621
+
622
+ az_check = [
623
+ (
624
+ [(t < az[1] and t > az[0]) for az in self._burst_az_boundaries]
625
+ if isinstance(t, PreciseDateTime)
626
+ else [False]
627
+ )
628
+ for t in t_azmth
629
+ ]
630
+ rng_check = [
631
+ [(t < rng[1] and t > rng[0]) for rng in self._burst_rng_boundaries] if ~np.isnan(t) else [False]
632
+ for t in t_rng
633
+ ]
634
+ check = [np.logical_and(az_check[c], rng_check[c]) for c in range(len(az_check))]
635
+
636
+ bursts = [list(np.where(c)[0]) if c.any() else None for c in check]
637
+
638
+ return bursts
639
+
640
+ def times_to_burst_association(self, azimuth_times: ArrayLike) -> list[int]:
641
+ """Associate the right burst to a given input time point. This function returns 1 association for each
642
+ input time.
643
+ Associating time only to the first burst containing it.
644
+
645
+ Parameters
646
+ ----------
647
+ azimuth_times : ArrayLike
648
+ azimuth time array in PreciseDateTime format
649
+
650
+ Returns
651
+ -------
652
+ list[int]
653
+ burst associated with a given time
654
+
655
+ Raises
656
+ ------
657
+ CoordinatesOutOfBounds
658
+ if input time exceeds tme boundaries of the swath
659
+ """
660
+ if self._channel.burst_info is None:
661
+ return [0] * len(azimuth_times)
662
+
663
+ bursts_start_times = self._channel.burst_info.azimuth_start_times
664
+ last_time = (
665
+ bursts_start_times[0]
666
+ + self._channel.burst_info.num
667
+ * self._channel.burst_info.lines_per_burst
668
+ * self._channel.raster_info.lines.step
669
+ )
670
+
671
+ bursts = []
672
+ for time in azimuth_times:
673
+ if time < bursts_start_times[0] or time > last_time:
674
+ raise CoordinatesOutOfBounds(f"{time} is out of the recorded timeline")
675
+
676
+ time_diff = time - bursts_start_times
677
+ time_mask = np.ma.masked_less(time_diff.astype("float64"), 0)
678
+ # associating time only to the first burst containing it
679
+ bursts.append(time_mask.argmin())
680
+
681
+ return bursts
682
+
683
+ def pixel_to_burst_association(self, azimuth_px_indexes: ArrayLike) -> list[int]:
684
+ """Associate the azimuth pixel value to the right burst. This function returns 1 association for each
685
+ input time.
686
+
687
+ Parameters
688
+ ----------
689
+ azimuth_px_indexes : ArrayLike
690
+ azimuth pixel indexes array
691
+
692
+ Returns
693
+ -------
694
+ list[int]
695
+ burst associated with a given pixel index
696
+
697
+ Raises
698
+ ------
699
+ CoordinatesOutOfBounds
700
+ if input time exceeds tme boundaries of the swath
701
+ """
702
+ if self._channel.burst_info is None:
703
+ return [0] * len(azimuth_px_indexes)
704
+
705
+ bursts_lines = np.repeat(self._channel.burst_info.lines_per_burst, self._channel.burst_info.num)
706
+ burst_boundaries = np.array([0] + [sum(bursts_lines[: t + 1]) for t, _ in enumerate(bursts_lines)])
707
+
708
+ bursts = []
709
+ for coord in azimuth_px_indexes:
710
+ if coord > burst_boundaries[-1]:
711
+ raise CoordinatesOutOfBounds(f"{coord} pixel exceeds swath's bounds")
712
+
713
+ px_diff = coord - burst_boundaries
714
+ px_mask = np.ma.masked_less(px_diff, 0)
715
+
716
+ bursts.append(px_mask.argmin())
717
+
718
+ return bursts
719
+
720
+ def read_data(
721
+ self,
722
+ azimuth_index: float,
723
+ range_index: float,
724
+ cropping_size: tuple[int, int] = (150, 150),
725
+ output_radiometric_quantity: SARRadiometricQuantity = SARRadiometricQuantity.BETA_NOUGHT,
726
+ burst: int | None = None,
727
+ ) -> np.ndarray:
728
+ """Extracting the swath portion centered to the provided target position and of size cropping_size by
729
+ cropping_size. Target position is provided via its azimuth and range indexes in the swath array.
730
+
731
+ Parameters
732
+ ----------
733
+ azimuth_index : float
734
+ index of azimuth time in swath array
735
+ range_index : float
736
+ index of range time in swath array
737
+ cropping_size : tuple[int, int], optional
738
+ size in pixel of the swath portion to be read (number of samples, number of lines), by default (150, 150)
739
+ output_radiometric_quantity : SARRadiometricQuantity, optional
740
+ selected output radiometric quantity to convert the read data to, if needed,
741
+ by default SARRadiometricQuantity.BETA_NOUGHT
742
+ burst : int, optional
743
+ if burst is provided, the roi extraction gives error if the boundaries exceed the burst boundaries,
744
+ by default None
745
+
746
+ Returns
747
+ -------
748
+ np.ndarray
749
+ cropped swath array centered to the input target coordinates, output array is (samples, lines)
750
+ by default the output radiometric quantity is BETA_NOUGHT, unless specified otherwise
751
+
752
+ Raises
753
+ ------
754
+ AzimuthExceedsBoundariesError
755
+ azimuth index exceeds swath boundaries
756
+ RangeExceedsBoundariesError
757
+ range index exceeds swath boundaries
758
+ """
759
+
760
+ # creating the target block identifier for partial swath reading
761
+ # [start line, start sample, number of lines, number of samples]
762
+ target_block = [
763
+ azimuth_index - np.floor(cropping_size[1] / 2).astype(int),
764
+ range_index - np.floor(cropping_size[0] / 2).astype(int),
765
+ cropping_size[1],
766
+ cropping_size[0],
767
+ ]
768
+
769
+ # full raster boundaries and burst boundaries, if applicable
770
+ raster_boundaries = [0, 0, self._channel.raster_info.lines.length, self._channel.raster_info.samples.length]
771
+ burst_boundaries = None
772
+ # if burst is provided, it means that the ROI to be read must be inside of this burst, otherwise the extracted
773
+ # data are not meaningful with respect to times, acquisition consistency and IRF
774
+ if burst is not None:
775
+ burst_boundaries = [
776
+ sum(self.lines_per_burst[:burst]),
777
+ 0,
778
+ self.lines_per_burst[burst],
779
+ self._channel.raster_info.samples.length,
780
+ ]
781
+
782
+ # validating target block extraction with respect to raster boundaries and burst boundaries
783
+ roi_validation(
784
+ roi=target_block,
785
+ raster_boundaries=raster_boundaries,
786
+ burst_boundaries=burst_boundaries,
787
+ )
788
+
789
+ # reading data portion and switching to convention (samples, lines) with transpose
790
+ data = read_channel_data(
791
+ raster_file=self._raster_file,
792
+ channel_id=self._channel_id,
793
+ block_to_read=target_block,
794
+ scaling_conversion=self._channel.image_calibration_factor,
795
+ ).T
796
+
797
+ # converting to beta nought if radiometric quantity is different
798
+ if self._radiometric_quantity != output_radiometric_quantity:
799
+ azimuth_time, _ = self.pixel_to_times_conversion(azimuth_index=azimuth_index, range_index=range_index)
800
+ incidence_angles = compute_incidence_angles(
801
+ trajectory=self.trajectory,
802
+ azimuth_time=azimuth_time,
803
+ range_times=self._slant_range_axis[target_block[1] : target_block[1] + target_block[3]],
804
+ look_direction=self.looking_side.value,
805
+ )
806
+ data = radiometric_correction(
807
+ data=data,
808
+ incidence_angle=incidence_angles,
809
+ input_quantity=self._radiometric_quantity,
810
+ output_quantity=output_radiometric_quantity,
811
+ )
812
+
813
+ return data
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: sct_cosmo_reader
3
+ Version: 1.0.0
4
+ Summary: SCT (SAR Calibration Toolbox) plugin for reading COSMO-SkyMed Level 1 product format.
5
+ Project-URL: Homepage, https://github.com/aresys-srl/sct_plugins
6
+ Project-URL: Repository, https://github.com/aresys-srl/sct_plugins
7
+ Project-URL: Documentation, https://aresys-srl.github.io/sct_plugins
8
+ Author-email: "Aresys S.R.L." <info@aresys.it>
9
+ License: MIT License
10
+
11
+ Copyright (C) Aresys S.r.l. <info@aresys.it>
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE.txt
31
+ Classifier: Development Status :: 5 - Production/Stable
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Intended Audience :: Education
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Natural Language :: English
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: 3.14
43
+ Classifier: Topic :: Scientific/Engineering
44
+ Classifier: Topic :: Software Development :: Libraries
45
+ Classifier: Topic :: Utilities
46
+ Classifier: Typing :: Typed
47
+ Requires-Python: >=3.11
48
+ Requires-Dist: eo-products==1.0.0
49
+ Requires-Dist: numpy>2
50
+ Requires-Dist: perseo-core==1.0.0
51
+ Requires-Dist: perseo-quality==1.0.0
52
+ Requires-Dist: scipy
53
+ Requires-Dist: sct
54
+ Requires-Dist: shapely>=2.1.0
55
+ Provides-Extra: dev
56
+ Requires-Dist: nox; extra == 'dev'
57
+ Requires-Dist: pylint; extra == 'dev'
58
+ Requires-Dist: ruff; extra == 'dev'
59
+ Provides-Extra: docs
60
+ Requires-Dist: mkdocstrings-python; extra == 'docs'
61
+ Requires-Dist: zensical; extra == 'docs'
62
+ Provides-Extra: test
63
+ Requires-Dist: pytest; extra == 'test'
64
+ Requires-Dist: pytest-cov; extra == 'test'
65
+ Requires-Dist: sct[graphs]; extra == 'test'
66
+ Description-Content-Type: text/markdown
67
+
68
+ # SCT Plugin: COSMO-SkyMed format reader
69
+
70
+ [![PyPI version](https://img.shields.io/pypi/v/sct-cosmo-reader)](https://pypi.org/project/sct-cosmo-reader/)
71
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://python.org)
72
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt)
73
+
74
+ [![CI](https://github.com/aresys-srl/sct_plugins/actions/workflows/cosmo.yml/badge.svg)](https://github.com/aresys-srl/sct_plugins/actions/workflows/cosmo.yml)
75
+
76
+ [SCT (SAR Calibration Toolbox)](https://github.com/aresys-srl/sct) plugin for reading COSMO-SkyMed
77
+ Level 1 products, both L1A (SLC) and L1B (GRD). This package integrates with SCT through its
78
+ input products plugin system, enabling all SCT analyses on COSMO-SkyMed data.
79
+
80
+ ## Supported Acquisition Modes
81
+
82
+ - **Stripmap**
83
+ - **Spotlight**
84
+ - **Scansar**
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install sct-cosmo-reader
90
+ ```
91
+
92
+ SCT is automatically installed as a dependency.
93
+
94
+ ## Compatibility
95
+
96
+ This plugin must be installed in the same Python environment as SCT. Once installed,
97
+ the plugin is automatically discovered and registered by SCT through its entry-point
98
+ based plugin system; no additional configuration is required.
99
+
100
+ ## Documentation
101
+
102
+ - [SCT documentation](https://aresys-srl.github.io/sct/)
103
+ - [Plugins documentation](https://aresys-srl.github.io/sct_plugins)
104
+
105
+ ## License
106
+
107
+ This project is licensed under the MIT License. See the [LICENSE.txt](LICENSE.txt) file for details.
108
+
109
+ Copyright &copy; 2026-present Aresys S.r.l. <info@aresys.it>
@@ -0,0 +1,8 @@
1
+ sct_cosmo_reader/__init__.py,sha256=f0XlJukfK3Wt-bqmG3G6MUiQCo-zf44rS7N9_FMdqJk,166
2
+ sct_cosmo_reader/interface.py,sha256=8wS2b7uXIEaVAkfLILnD1zR2oTzCw_DJvov65qzDZwU,938
3
+ sct_cosmo_reader/protocol_implementation.py,sha256=N122jh6ndD1usMo9dn2KYWUYHTBn_bxoavYldiKkVeg,30021
4
+ sct_cosmo_reader-1.0.0.dist-info/METADATA,sha256=Clebj5URM8h3QXQcVk7miCNonjT2AzwLAgijDbQsulA,4618
5
+ sct_cosmo_reader-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ sct_cosmo_reader-1.0.0.dist-info/entry_points.txt,sha256=UJFP5Krj0xPMvuD_9GEBSkeE-OszfXLv3Q00ZFezu1c,75
7
+ sct_cosmo_reader-1.0.0.dist-info/licenses/LICENSE.txt,sha256=5-eKUp948IoydappLufHc2oAqasNGKhXpIB3qqQvXlY,1082
8
+ sct_cosmo_reader-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [sct.input_products]
2
+ cosmo = sct_cosmo_reader.interface:COSMOProductPlugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (C) Aresys S.r.l. <info@aresys.it>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.