sct-asar-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: GPLv3+
3
+
4
+ """SCT-Plugin: ENVISAT/ERS product in ASAR format reader."""
5
+
6
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: GPLv3+
3
+
4
+ """Envisat & ERS format reader."""
@@ -0,0 +1,187 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: GPLv3+
3
+
4
+ """Common Enum, dataclasses and other utilities."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum, auto
10
+ from typing import Literal
11
+
12
+ import numpy as np
13
+ from numpy.polynomial import Polynomial
14
+ from perseo_core.timing import PreciseDateTime
15
+ from scipy.interpolate import CubicSpline
16
+
17
+ LookingDirection = Literal["LEFT", "RIGHT"]
18
+
19
+
20
+ class SARRadiometricQuantity(Enum):
21
+ """Enum class for radiometric analysis input/output quantity types"""
22
+
23
+ BETA_NOUGHT = auto()
24
+ SIGMA_NOUGHT = auto()
25
+ GAMMA_NOUGHT = auto()
26
+
27
+
28
+ class SARPolarization(Enum):
29
+ """Polarization enum class"""
30
+
31
+ HH = "H/H"
32
+ VV = "V/V"
33
+ HV = "H/V"
34
+ VH = "V/H"
35
+
36
+
37
+ class SARProjection(Enum):
38
+ """Enum class for managing swath projection of product folder"""
39
+
40
+ SLANT_RANGE = "SLANT RANGE"
41
+ GROUND_RANGE = "GROUND RANGE"
42
+
43
+
44
+ class OrbitDirection(Enum):
45
+ """Orbit direction: ascending or descending"""
46
+
47
+ ASCENDING = "ascending"
48
+ DESCENDING = "descending"
49
+
50
+
51
+ class StandardSARAcquisitionMode(Enum):
52
+ """Standard cross-package SAR acquisition mode definition"""
53
+
54
+ SCANSAR = auto()
55
+ SPOTLIGHT = auto()
56
+ STRIPMAP = auto()
57
+ TOPSAR = auto()
58
+ WAVE = auto()
59
+ UNKNOWN = auto()
60
+
61
+
62
+ @dataclass
63
+ class PulseInfo:
64
+ """Chirp pulse info"""
65
+
66
+ length_s: float
67
+ bandwidth_hz: float
68
+ sampling_rate_hz: float
69
+ energy_j: float
70
+ start_frequency_hz: float
71
+ start_phase: float
72
+ direction: str
73
+
74
+
75
+ @dataclass
76
+ class DatasetInfo:
77
+ """Dataset Info"""
78
+
79
+ fc_hz: float
80
+ acquisition_mode: str
81
+ sensor_name: str
82
+ image_type: str
83
+ projection: str
84
+ side_looking: LookingDirection
85
+
86
+
87
+ @dataclass
88
+ class RasterInfo:
89
+ """Product Raster Info"""
90
+
91
+ lines: RasterInfoAxis
92
+ samples: RasterInfoAxis
93
+ data_type: str | None = None
94
+ raster_name: str | None = None
95
+
96
+
97
+ @dataclass
98
+ class RasterInfoAxis:
99
+ """Axis representation for RasterInfo"""
100
+
101
+ length: int
102
+ step: float
103
+ start: float | PreciseDateTime
104
+ step_unit: str
105
+ axis: np.ndarray = field(init=False)
106
+
107
+ def __post_init__(self):
108
+ # generating axis array from inputs
109
+ self.axis = np.arange(0, self.length, 1) * self.step + self.start
110
+
111
+
112
+ @dataclass
113
+ class SARSamplingFrequencies:
114
+ """SAR signal sampling frequencies"""
115
+
116
+ range_freq_hz: float
117
+ range_bandwidth_freq_hz: float
118
+ azimuth_freq_hz: float
119
+ azimuth_bandwidth_freq_hz: float
120
+
121
+
122
+ @dataclass
123
+ class ConversionFunction:
124
+ """Generic conversion function wrapper"""
125
+
126
+ azimuth_reference_time: PreciseDateTime
127
+ origin: float
128
+ function: Polynomial | CubicSpline
129
+
130
+
131
+ @dataclass
132
+ class ConversionPolynomial:
133
+ """Generic conversion polynomial wrapper"""
134
+
135
+ azimuth_reference_time: PreciseDateTime
136
+ origin: float
137
+ polynomial: Polynomial | CubicSpline
138
+
139
+
140
+ @dataclass
141
+ class DopplerEvaluator:
142
+ """Doppler function (rate/centroid) evaluator"""
143
+
144
+ functions: list[ConversionFunction] | None = None
145
+ azimuth_reference_times: np.ndarray | None = None
146
+
147
+ def evaluate(self, azimuth_time: PreciseDateTime, slant_range: np.ndarray) -> np.ndarray:
148
+ """Evaluate function at given inputs.
149
+
150
+ Parameters
151
+ ----------
152
+ azimuth_time : PreciseDateTime
153
+ azimuth time to select the proper function
154
+ slant_range : np.ndarray
155
+ slant range value(s) in meters
156
+
157
+ Returns
158
+ -------
159
+ np.ndarray
160
+ Doppler functions values at slant range
161
+ """
162
+ poly_index = detect_right_polynomial_index(
163
+ azimuth_time=azimuth_time, reference_azimuth_times=self.azimuth_reference_times
164
+ )
165
+ poly = self.functions[poly_index]
166
+ return poly.function(slant_range - poly.origin)
167
+
168
+
169
+ def detect_right_polynomial_index(azimuth_time: PreciseDateTime, reference_azimuth_times: np.ndarray) -> int:
170
+ """Detecting the index of the right polynomial to be used given an input azimuth time.
171
+ The polynomial to be used is the one with reference azimuth time closest to the input value but with
172
+ reference_azimuth_time < input_azimuth_time.
173
+
174
+ Parameters
175
+ ----------
176
+ azimuth_time : PreciseDateTime
177
+ selected azimuth time
178
+ reference_azimuth_times : np.ndarray
179
+ array of reference azimuth times, in PreciseDateTime format
180
+
181
+ Returns
182
+ -------
183
+ int
184
+ index corresponding to the polynomial to be used
185
+ """
186
+ diff = np.array(azimuth_time - reference_azimuth_times).astype("float")
187
+ return np.ma.masked_where(diff < 0, diff).argmin()
@@ -0,0 +1,215 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: GPLv3+
3
+
4
+ """Envisat & ERS product format reader."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import epr
11
+ import numpy as np
12
+
13
+ import sct_asar_reader.core.utilities as support
14
+ from sct_asar_reader.core.common import SARRadiometricQuantity, StandardSARAcquisitionMode
15
+
16
+
17
+ def read_channel_metadata(file_path: Path | str, channel_id: str) -> support.ASARChannelMetadata:
18
+ """Reading metadata for current ASAR input product related to the selected channel.
19
+
20
+ Parameters
21
+ ----------
22
+ file_path : Path | str
23
+ Path to the ASAR binary product
24
+ channel_id : str
25
+ channel of choice
26
+
27
+ Returns
28
+ -------
29
+ support.ASARChannelMetadata
30
+ metadata for the selected channel
31
+ """
32
+ file_path = Path(file_path)
33
+ mph, sph = support.read_product_headers(product=file_path)
34
+ acquisition_mode = support.ASARAcquisitionMode.from_str(file_path.name)
35
+ if acquisition_mode == support.ASARAcquisitionMode.STRIPMAP:
36
+ acquisition_mode_std = StandardSARAcquisitionMode.STRIPMAP
37
+ else:
38
+ acquisition_mode_std = StandardSARAcquisitionMode.WAVE
39
+ product_type = support.ASARProductType.from_str(sph.sample_type)
40
+ projection = support.get_projection_from_product_type(product_type)
41
+
42
+ product = epr.open(str(file_path))
43
+ main_params_dataset = product.get_dataset("MAIN_PROCESSING_PARAMS_ADS")
44
+ doppler_centroid_coeffs_dataset = product.get_dataset("DOP_CENTROID_COEFFS_ADS")
45
+ geolocation_grid_dataset = product.get_dataset("GEOLOCATION_GRID_ADS")
46
+
47
+ # mds1_sq_dataset = product.get_dataset("MDS1_SQ_ADS")
48
+ # mds1_dataset = product.get_dataset("MDS1")
49
+ # chirp_params_dataset = product.get_dataset("CHIRP_PARAMS_ADS")
50
+
51
+ raster_info = support.raster_info_from_record(
52
+ main_params_dataset=main_params_dataset,
53
+ geolocation_record=geolocation_grid_dataset.read_record(0),
54
+ product_type=product_type,
55
+ )
56
+
57
+ state_vectors = support.ASARStateVectors.from_metadata(
58
+ main_params_dataset=main_params_dataset, orbit_direction=sph.orbit_direction
59
+ )
60
+ orbit = support.orbit_from_state_vectors(state_vectors=state_vectors)
61
+
62
+ doppler_centroid_poly = support.doppler_centroid_poly_from_dataset(
63
+ dc_params_dataset=doppler_centroid_coeffs_dataset
64
+ )
65
+ doppler_rate_poly = support.doppler_rate_poly_from_dataset(
66
+ main_params_dataset=main_params_dataset,
67
+ )
68
+
69
+ coordinate_conversion = None
70
+ if "SR_GR_ADS" in product.get_dataset_names():
71
+ sr_gr_dataset = product.get_dataset("SR_GR_ADS")
72
+ coordinate_conversion = support.ASARCoordinateConversions.from_dataset(
73
+ slant_to_ground_dataset=sr_gr_dataset, raster_info=raster_info
74
+ )
75
+ else:
76
+ coordinate_conversion = support.ASARCoordinateConversions.from_orbit(orbit=orbit, raster_info=raster_info)
77
+
78
+ ds_channel_index = [p.name.lower() for p in sph.mds_polarizations].index(channel_id.split("_")[-1])
79
+ for ds_id, main_processing_params in enumerate(main_params_dataset):
80
+ if not ds_id == ds_channel_index:
81
+ continue
82
+
83
+ burst_info = support.ASARBurstInfo(
84
+ num=1,
85
+ lines_per_burst=raster_info.lines.length,
86
+ samples_per_burst=raster_info.samples.length,
87
+ azimuth_start_times=np.array([raster_info.lines.start]),
88
+ range_start_times=np.array([raster_info.samples.start]),
89
+ )
90
+
91
+ dataset_info = support.dataset_info_from_record(
92
+ product_type=product_type,
93
+ acquisition_mode=acquisition_mode,
94
+ main_params_record=main_processing_params,
95
+ mph=mph,
96
+ sph=sph,
97
+ )
98
+ sampling_constants = support.sampling_constants_from_record(main_params_record=main_processing_params)
99
+ swath_info = support.ASARSwathInfo.from_record(main_params_record=main_processing_params)
100
+ general_info = support.ASARGeneralChannelInfo(
101
+ product_name=mph.product,
102
+ channel_id=channel_id,
103
+ swath=sph.swath,
104
+ product_type=product_type,
105
+ polarization=sph.mds_polarizations[ds_id],
106
+ projection=projection,
107
+ acquisition_mode=acquisition_mode,
108
+ acquisition_mode_std=acquisition_mode_std,
109
+ orbit_direction=sph.orbit_direction,
110
+ signal_frequency=dataset_info.fc_hz,
111
+ acq_start_time=mph.sensing_start,
112
+ acq_stop_time=mph.sensing_stop,
113
+ )
114
+
115
+ calibration_factor = support.get_calibration_factor(index=ds_id, main_params_record=main_processing_params)
116
+
117
+ pulse_info = support.pulse_info_from_record(main_params_record=main_processing_params)
118
+
119
+ product.close()
120
+ return support.ASARChannelMetadata(
121
+ general_info=general_info,
122
+ orbit=orbit,
123
+ image_calibration_factor=calibration_factor,
124
+ image_radiometric_quantity=SARRadiometricQuantity.BETA_NOUGHT,
125
+ raster_info=raster_info,
126
+ burst_info=burst_info,
127
+ dataset_info=dataset_info,
128
+ swath_info=swath_info,
129
+ sampling_constants=sampling_constants,
130
+ doppler_centroid_poly=doppler_centroid_poly,
131
+ doppler_rate_poly=doppler_rate_poly,
132
+ pulse=pulse_info,
133
+ coordinate_conversions=coordinate_conversion,
134
+ state_vectors=state_vectors,
135
+ )
136
+
137
+
138
+ def read_channel_data(raster_file: str | Path, block_to_read: list[int] | None = None, scaling_conversion: float = 1):
139
+ """Reading ASAR data file from binary.
140
+
141
+ NOTE: the range axis is actually reversed. When reading data, the ROI range index should be provided relative to
142
+ the end of the raster range axis and not to the start, i.e. range_roi_index = samples - range_roi_index.
143
+ Data portion is the flip alongside range direction and returned to the user.
144
+
145
+ Parameters
146
+ ----------
147
+ raster_file : str | Path
148
+ Path to .N1 binary file
149
+ block_to_read : list[int] | None, optional
150
+ data block to be read, to be specified as a list of 4 integers, in the form:
151
+
152
+ 0. first line to be read
153
+ 1. first sample to be read
154
+ 2. total number of lines to be read
155
+ 3. total number of samples to be read
156
+
157
+ by default None
158
+ scaling_conversion : float, optional
159
+ scaling conversion to be applied to the data read
160
+
161
+ Returns
162
+ -------
163
+ np.ndarray
164
+ numpy array containing the data read from raster file, with shape (lines, samples)
165
+ """
166
+
167
+ product = epr.open(str(raster_file))
168
+ if "proc_data" not in product.get_band_names():
169
+ i_data = product.get_band("i")
170
+ q_data = product.get_band("q")
171
+ if block_to_read is not None:
172
+ i_data_area = i_data.read_as_array(
173
+ block_to_read[3], block_to_read[2], xoffset=block_to_read[1], yoffset=block_to_read[0]
174
+ )
175
+ q_data_area = q_data.read_as_array(
176
+ block_to_read[3], block_to_read[2], xoffset=block_to_read[1], yoffset=block_to_read[0]
177
+ )
178
+ else:
179
+ i_data_area = i_data.read_as_array()
180
+ q_data_area = q_data.read_as_array()
181
+ target_area = q_data_area + 1j * i_data_area
182
+ else:
183
+ proc_data = product.get_band("proc_data")
184
+ if block_to_read is not None:
185
+ target_area = proc_data.read_as_array(
186
+ block_to_read[3], block_to_read[2], xoffset=block_to_read[1], yoffset=block_to_read[0]
187
+ )
188
+ else:
189
+ target_area = proc_data.read_as_array()
190
+
191
+ product.close()
192
+ # NOTE: flipping along range due to reversed range raster direction
193
+ target_area = np.flip(target_area, axis=1)
194
+
195
+ return target_area * scaling_conversion
196
+
197
+
198
+ def open_product(pf_path: str | Path) -> support.ASARProduct:
199
+ """Open a ASAR product.
200
+
201
+ Parameters
202
+ ----------
203
+ pf_path : str | Path
204
+ Path to the ASAR product
205
+
206
+ Returns
207
+ -------
208
+ ASARProduct
209
+ ASARProduct object corresponding to the input ASAR product
210
+ """
211
+
212
+ if not support.is_asar_product(product=pf_path):
213
+ raise support.InvalidASARProductError(f"{pf_path}")
214
+
215
+ return support.ASARProduct(path=pf_path)