arepytools 1.7.6__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.
Files changed (53) hide show
  1. arepytools/__init__.py +8 -0
  2. arepytools/constants.py +109 -0
  3. arepytools/geometry/__init__.py +6 -0
  4. arepytools/geometry/_geodetic.py +107 -0
  5. arepytools/geometry/_interpolator.py +210 -0
  6. arepytools/geometry/anx_time.py +134 -0
  7. arepytools/geometry/attitude_utils.py +88 -0
  8. arepytools/geometry/conversions.py +120 -0
  9. arepytools/geometry/curve.py +344 -0
  10. arepytools/geometry/curve_protocols.py +113 -0
  11. arepytools/geometry/direct_geocoding.py +1062 -0
  12. arepytools/geometry/ellipsoid.py +233 -0
  13. arepytools/geometry/generalsarattitude.py +528 -0
  14. arepytools/geometry/generalsarorbit.py +831 -0
  15. arepytools/geometry/geometric_functions.py +493 -0
  16. arepytools/geometry/inverse_geocoding.py +315 -0
  17. arepytools/geometry/inverse_geocoding_core.py +806 -0
  18. arepytools/geometry/orbit.py +160 -0
  19. arepytools/geometry/reference_frames.py +289 -0
  20. arepytools/geometry/rotation.py +144 -0
  21. arepytools/geometry/wgs84.py +22 -0
  22. arepytools/io/__init__.py +59 -0
  23. arepytools/io/channel.py +513 -0
  24. arepytools/io/channel_iteration.py +147 -0
  25. arepytools/io/create_orbit.py +35 -0
  26. arepytools/io/debugio.py +65 -0
  27. arepytools/io/io_support.py +683 -0
  28. arepytools/io/manifest.py +100 -0
  29. arepytools/io/metadata.py +2679 -0
  30. arepytools/io/parsing/__init__.py +6 -0
  31. arepytools/io/parsing/metadata_models/__init__.py +93 -0
  32. arepytools/io/parsing/metadata_models/aresys_generic_metadata.py +222 -0
  33. arepytools/io/parsing/metadata_models/aresys_types.py +3391 -0
  34. arepytools/io/parsing/metadata_parsing.py +49 -0
  35. arepytools/io/parsing/parsing.py +70 -0
  36. arepytools/io/parsing/translate.py +1701 -0
  37. arepytools/io/point_target_binary.py +406 -0
  38. arepytools/io/point_target_file.py +299 -0
  39. arepytools/io/point_target_models/__init__.py +25 -0
  40. arepytools/io/point_target_models/aresys_point_target_models.py +181 -0
  41. arepytools/io/productfolder.py +467 -0
  42. arepytools/io/productfolder2.py +538 -0
  43. arepytools/io/productfolder_layout.py +184 -0
  44. arepytools/math/__init__.py +6 -0
  45. arepytools/math/axis.py +434 -0
  46. arepytools/math/genericpoly.py +155 -0
  47. arepytools/timing/__init__.py +6 -0
  48. arepytools/timing/conversions.py +54 -0
  49. arepytools/timing/precisedatetime.py +863 -0
  50. arepytools-1.7.6.dist-info/METADATA +64 -0
  51. arepytools-1.7.6.dist-info/RECORD +53 -0
  52. arepytools-1.7.6.dist-info/WHEEL +4 -0
  53. arepytools-1.7.6.dist-info/licenses/LICENSE.txt +21 -0
arepytools/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ ArePyTools: the Python toolkit for SAR data processing
6
+ """
7
+
8
+ __version__ = "1.7.6"
@@ -0,0 +1,109 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+
5
+ """
6
+ **Deprecated** Constants module
7
+ -------------------------------
8
+
9
+ .. deprecated:: v1.7.0
10
+ Importing constants is deprecated.
11
+ Use scipy.constants.speed_of_light for LIGHT_SPEED.
12
+
13
+ Examples
14
+ --------
15
+ >>> import arepytools.constants as cst
16
+ >>> print(cst.LIGHT_SPEED)
17
+ 299792458.0
18
+ """
19
+ import warnings
20
+
21
+ import scipy
22
+
23
+ warnings.warn(
24
+ "arepytools.constants module is deprecated. Instead of importing LIGHT_SPEED use 'from scipy.constants import speed_of_light'",
25
+ DeprecationWarning,
26
+ stacklevel=2,
27
+ )
28
+
29
+ SECONDS_IN_A_DAY = 86164.09054
30
+ """
31
+ Number of seconds in a day
32
+ """
33
+
34
+ LIGHT_SPEED = scipy.constants.speed_of_light
35
+ """
36
+ Speed of light in vacuum (m/s).
37
+ """
38
+
39
+ # Units of measure
40
+ SECOND_STR = "s"
41
+ """
42
+ Second symbol.
43
+ """
44
+
45
+ HERTZ_STR = "Hz"
46
+ """
47
+ Hertz symbol.
48
+ """
49
+
50
+ JOULE_STR = "j"
51
+ """
52
+ Joule symbol.
53
+ """
54
+
55
+ DEGREE_STR = "deg"
56
+ """
57
+ Degree symbol.
58
+ """
59
+
60
+ RAD_STR = "rad"
61
+ """
62
+ Radian symbol.
63
+ """
64
+
65
+ UTC_STR = "Utc"
66
+ """
67
+ Coordinated Universal Time abbreviation.
68
+ """
69
+
70
+ METER_STR = "m"
71
+ """
72
+ Meter symbol.
73
+ """
74
+
75
+ # Metric prefixes
76
+ KILO = 1e3
77
+ """
78
+ Number of units in one kilounit (kilounits-to-units conversion factor).
79
+ """
80
+
81
+ MEGA = KILO**2
82
+ """
83
+ Number of units in one megaunit (megaunits-to-units conversion factor).
84
+ """
85
+
86
+ GIGA = KILO**3
87
+ """
88
+ Number of units in one gigaunit (gigaunits-to-units conversion factor).
89
+ """
90
+
91
+ MILLI = KILO**-1
92
+ """
93
+ Number of units in one milliunit (milliunits-to-units conversion factor).
94
+ """
95
+
96
+ MICRO = KILO**-2
97
+ """
98
+ Number of units in one microunit (microunits-to-units conversion factor).
99
+ """
100
+
101
+ NANO = KILO**-3
102
+ """
103
+ Number of units in one nanounit (nanounits-to-units conversion factor).
104
+ """
105
+
106
+ PICO = KILO**-4
107
+ """
108
+ Number of units in one picounit (picounits-to-units conversion factor).
109
+ """
@@ -0,0 +1,6 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Geometry management package
6
+ """
@@ -0,0 +1,107 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Geodetic module
6
+ ---------------
7
+ """
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+
12
+ from arepytools.geometry.conversions import llh2xyz, xyz2llh
13
+ from arepytools.geometry.ellipsoid import WGS84
14
+
15
+ _semi_axis_ratio_sqr = WGS84.semi_axes_ratio_min_max**2
16
+ _major_semi_axis_sqr = WGS84.semi_major_axis**2
17
+ _minor_semi_axis_sqr = WGS84.semi_minor_axis**2
18
+
19
+
20
+ def _ze(x: float, y: float) -> float:
21
+ eps = 1.0e-21
22
+
23
+ return np.sqrt(_minor_semi_axis_sqr - _semi_axis_ratio_sqr * (x**2 + y**2)) + eps
24
+
25
+
26
+ def _ze_x(x: float, y: float) -> float:
27
+ return -WGS84.semi_axes_ratio_min_max * x / _ze(x, y)
28
+
29
+
30
+ def _ze_xy(x: float, y: float) -> float:
31
+ # pylint: disable-next=arguments-out-of-order
32
+ return (_semi_axis_ratio_sqr * x) / _ze(x, y) ** 2 * _ze_x(y, x)
33
+
34
+
35
+ def _ze_xx(x: float, y: float) -> float:
36
+ return -_semi_axis_ratio_sqr / _ze(x, y) + (_semi_axis_ratio_sqr * x) / _ze(x, y) ** 2 * _ze_x(x, y)
37
+
38
+
39
+ def _compute_geodetic_jacobian(point: np.ndarray, sensor_position: np.ndarray) -> np.ndarray:
40
+ jac = np.empty(shape=(3, 3), dtype=float)
41
+
42
+ zed_diff = point[2] - sensor_position[2]
43
+
44
+ jac[0][0] = 2.0 * point[0] / _major_semi_axis_sqr
45
+ jac[0][1] = 2.0 * point[1] / _major_semi_axis_sqr
46
+ jac[0][2] = 2.0 * point[2] / _minor_semi_axis_sqr
47
+ jac[1][0] = 1.0 + zed_diff * _ze_xx(point[0], point[1])
48
+ jac[1][1] = zed_diff * _ze_xy(point[0], point[1])
49
+ jac[1][2] = _ze_x(point[0], point[1])
50
+ jac[2][0] = jac[1][1]
51
+ jac[2][1] = 1.0 + zed_diff * _ze_xx(point[1], point[0])
52
+ jac[2][2] = _ze_x(point[1], point[0])
53
+
54
+ return jac
55
+
56
+
57
+ def _compute_geodetic_rhs(point: npt.NDArray[np.floating], sensor_position: npt.NDArray[np.floating]) -> np.ndarray:
58
+ rhs = np.empty(shape=(3,), dtype=float)
59
+ los = point - sensor_position
60
+ rhs[0] = (point[0] ** 2 + point[1] ** 2) / _major_semi_axis_sqr + point[2] ** 2 / _minor_semi_axis_sqr - 1
61
+ rhs[1] = los[0] + los[2] * _ze_x(point[0], point[1])
62
+ rhs[2] = los[1] + los[2] * _ze_x(point[1], point[0])
63
+ return rhs
64
+
65
+
66
+ def compute_geodetic_point(sensor_positions: npt.ArrayLike) -> np.ndarray:
67
+ """Compute the geodetic point that corresponds to a sensor position
68
+
69
+ :param sensor_position: sensor position array like with shape (3,) or (N, 3)
70
+
71
+ :return: geodetic point as a (3,) or (N, 3) numpy array
72
+ """
73
+ sensor_positions = np.asarray(sensor_positions)
74
+
75
+ if sensor_positions.ndim > 2 or sensor_positions.shape[-1] != 3:
76
+ raise ValueError(f"sensor_positions has invalid shape: {sensor_positions.shape}, it should be (3,) or (N, 3)")
77
+
78
+ sensor_positions = sensor_positions.copy()
79
+ change_sign = sensor_positions[..., 2] < 0
80
+ sensor_positions[change_sign, 2] *= -1
81
+
82
+ geodetic_points = xyz2llh(sensor_positions.T)
83
+ geodetic_points[2, ...] = 0
84
+ geodetic_points = llh2xyz(geodetic_points).T.reshape(sensor_positions.shape)
85
+
86
+ for sensor_position, point in zip(
87
+ sensor_positions.reshape((-1, 3)),
88
+ geodetic_points.reshape((-1, 3)),
89
+ ):
90
+ increment_norm_threshold = 1e-9
91
+ max_num_iterations = 10
92
+ for _ in range(max_num_iterations):
93
+ jacobian = _compute_geodetic_jacobian(point[:], sensor_position)
94
+ rhs = _compute_geodetic_rhs(point[:], sensor_position)
95
+
96
+ increment = -np.linalg.solve(jacobian, rhs)
97
+
98
+ increment_norm = np.linalg.norm(increment)
99
+
100
+ point[:] += increment
101
+
102
+ if increment_norm < increment_norm_threshold:
103
+ break
104
+
105
+ geodetic_points[change_sign, 2] *= -1
106
+
107
+ return geodetic_points
@@ -0,0 +1,210 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Geometry interpolator module
6
+ ----------------------------
7
+ """
8
+
9
+ import numpy as np
10
+
11
+
12
+ def check_eval_function_input(fun):
13
+ """
14
+ Interpolator eval function input checker
15
+ """
16
+
17
+ def decorated_fun(self, *args_f, **kwargs_f):
18
+ if isinstance(args_f[0], (list, np.ndarray)):
19
+ interpolation_axis = np.asarray(args_f[0])
20
+ else:
21
+ interpolation_axis = np.full((1,), args_f[0])
22
+ return fun(self, interpolation_axis, *args_f[1:], **kwargs_f)
23
+
24
+ return decorated_fun
25
+
26
+
27
+ class GeometryInterpolator:
28
+ """
29
+ Internal class for handling interpolation in GeneralSarOrbit and in GeneralSarAttitude
30
+ It interpolates vector-valued functions defined on a given axis
31
+ """
32
+
33
+ _POLYNOMIAL_ORDER = 4
34
+ _DATA_POINTS_PER_POLY = _POLYNOMIAL_ORDER + 1
35
+ _POLY_CENTER_REL_INTERVAL_INDEX = _DATA_POINTS_PER_POLY // 2
36
+
37
+ @classmethod
38
+ def get_min_num_of_data_points(cls):
39
+ """Return the minimum number of data points needed by the interpolator
40
+
41
+ :return: minimum number of data points"""
42
+ return cls._DATA_POINTS_PER_POLY
43
+
44
+ @classmethod
45
+ def get_polynomial_order(cls):
46
+ """Return the order of the interpolation polynomial
47
+
48
+ :return: order of the interpolation polynomial
49
+ """
50
+ return cls._POLYNOMIAL_ORDER
51
+
52
+ def __init__(self, data_axis, data):
53
+ """Initialize the object as interpolator of the input data defined on the specified axis
54
+
55
+ :param data_axis: the axis of length N on which the data is defined
56
+ :param data: data defined as MxN array of M-dimensional values (e.g. state vectors or attitude angles)
57
+ """
58
+ self._data_axis = data_axis
59
+ self._num_of_data_components = data.shape[0]
60
+ self._num_of_polynomials = self._data_axis.size - self._POLYNOMIAL_ORDER
61
+
62
+ # Matrix of polynomials
63
+ self._pos = np.ndarray((self._num_of_polynomials, self._num_of_data_components), dtype=np.poly1d)
64
+ self._vel = np.ndarray((self._num_of_polynomials, self._num_of_data_components), dtype=np.poly1d)
65
+ self._acc = np.ndarray((self._num_of_polynomials, self._num_of_data_components), dtype=np.poly1d)
66
+ # Relative azimuth axis [s]
67
+ relative_axis = self._data_axis.get_relative_array()
68
+ self._data_axis_array = self._data_axis.get_array()
69
+ for poly_index in range(self._num_of_polynomials):
70
+ # Each polynomial domain is defined by the poly_axis
71
+ poly_axis = relative_axis[poly_index : poly_index + self._DATA_POINTS_PER_POLY]
72
+
73
+ # Each polynomial relative axis starts at the start of the interval:
74
+ # P(t) = c0 + c1 * (t-poly_time_axis[0]) + ...
75
+ vandermonde_matrix = np.vander((poly_axis - poly_axis[0]).astype(float))
76
+
77
+ # Computation of polynomial coefficients
78
+ for component_index in range(self._num_of_data_components):
79
+ values = data[
80
+ component_index,
81
+ poly_index : poly_index + self._DATA_POINTS_PER_POLY,
82
+ ]
83
+ coefficients = np.linalg.solve(vandermonde_matrix, values).tolist()
84
+ position = np.poly1d(coefficients)
85
+ self._pos[poly_index, component_index] = position
86
+ self._vel[poly_index, component_index] = np.polyder(position, 1)
87
+ self._acc[poly_index, component_index] = np.polyder(position, 2)
88
+
89
+ @check_eval_function_input
90
+ def eval(self, interpolation_axis, interval_indexes, components_to_interpolate=None):
91
+ """Interpolate the data on the given interpolation axis
92
+
93
+ :param interpolation_axis: interpolation axis
94
+ :param interval_indexes: vector or single scalar or None. Denotes the intervals where the given interpolation
95
+ points are expected
96
+ :param components_to_interpolate: list of the components to interpolate, identified by their
97
+ index [optional]
98
+
99
+ :return: CxP array of interpolated values, where C is the number of interpolated components and P is the length
100
+ of the interpolation axis
101
+ """
102
+ return self._eval(interpolation_axis, interval_indexes, self._pos, components_to_interpolate)
103
+
104
+ @check_eval_function_input
105
+ def eval_first_derivative(self, interpolation_axis, interval_indexes, components_to_interpolate=None):
106
+ """Interpolate the first derivative of the data on the given interpolation axis
107
+
108
+ :param interpolation_axis: interpolation axis
109
+ :param interval_indexes: vector or single scalar or None. Denotes the interval where the given interpolation
110
+ points are expected
111
+ :param components_to_interpolate: list of the components to interpolate, identified by their index [optional]
112
+
113
+ :return: CxP array of interpolated values, where C is the number of interpolated components and P is the length
114
+ of the interpolation axis
115
+ """
116
+ return self._eval(interpolation_axis, interval_indexes, self._vel, components_to_interpolate)
117
+
118
+ @check_eval_function_input
119
+ def eval_second_derivative(self, interpolation_axis, interval_indexes, components_to_interpolate=None):
120
+ """Interpolate the second derivative of the data on the given interpolation axis
121
+
122
+ :param interpolation_axis: interpolation axis
123
+ :param interval_indexes: vector or single scalar or None. Denotes the interval where the given interpolation
124
+ points are expected
125
+ :param components_to_interpolate: list of the components to interpolate, identified by their index [optional]
126
+
127
+ :return: CxP array of interpolated values, where C is the number of interpolated components and P is the length
128
+ of the interpolation axis
129
+ """
130
+
131
+ return self._eval(interpolation_axis, interval_indexes, self._acc, components_to_interpolate)
132
+
133
+ def _eval(
134
+ self,
135
+ interpolation_axis,
136
+ interval_indexes,
137
+ polynomial,
138
+ components_to_interpolate=None,
139
+ ):
140
+ """Interpolate the data on the given interpolation axis by evaluating interpolation
141
+ polynomials (or a their modified version) on the interpolation axis
142
+
143
+ :param interpolation_axis: interpolation axis
144
+ :param interval_indexes: vector or single scalar or None. Denotes the interval where the given time is expected
145
+ :param polynomial: interpolation polynomials to evaluate
146
+ :param components_to_interpolate: list of the components to interpolate, identified by their index [optional]
147
+
148
+ :return: CxP array of interpolated values, where C is the number of interpolated components and P is the length
149
+ of the interpolation axis
150
+ """
151
+
152
+ if components_to_interpolate is None:
153
+ components_to_interpolate = range(self._num_of_data_components)
154
+
155
+ poly_indexes = self.get_polynomial_index(
156
+ interpolation_points=interpolation_axis, interval_indexes=interval_indexes
157
+ )
158
+
159
+ interpolation_results = np.zeros((len(components_to_interpolate), interpolation_axis.size))
160
+
161
+ for interpolation_point_index, (poly_index, interpolation_point) in enumerate(
162
+ zip(poly_indexes, interpolation_axis)
163
+ ):
164
+ interpolation_point_poly_rel = interpolation_point - self._data_axis_array[poly_index]
165
+
166
+ for out_component_index, component_index in enumerate(components_to_interpolate):
167
+ interpolation_results[out_component_index, interpolation_point_index] = polynomial[
168
+ poly_index, component_index
169
+ ](interpolation_point_poly_rel)
170
+
171
+ return interpolation_results
172
+
173
+ def get_interval_index(self, interpolation_points):
174
+ """Get the indexes of the intervals where the interpolation_points lie
175
+
176
+ :param interpolation_points: vector of required interpolation points
177
+
178
+ :return: array of interval indexes
179
+ """
180
+ return self._data_axis.get_interval_id(interpolation_points)
181
+
182
+ def get_polynomial_index(self, interpolation_points=None, interval_indexes=None):
183
+ """Get the indexes of the polynomials to be used for interpolation
184
+
185
+ :param interpolation_points: vector of required interpolation points
186
+ :param interval_indexes: interval indexes of the interpolation points
187
+
188
+ :return: array of polynomial indexes
189
+
190
+ If interpolation points are specified, the positions on the interpolation axis are computed internally to find
191
+ the corresponding polynomial indexes;
192
+ If the interval indexes of the interpolation points on the axis are given, the polynomial indexes are computed
193
+ directly (ignoring the interpolation points).
194
+ """
195
+ if interval_indexes is None:
196
+ if interpolation_points is not None:
197
+ interval_indexes = self._data_axis.get_interval_id(interpolation_points)
198
+ else:
199
+ raise RuntimeError("Specify either interpolation points or interval indexes")
200
+ if isinstance(interval_indexes, int):
201
+ interval_indexes = np.asarray([interval_indexes])
202
+ return np.clip(
203
+ interval_indexes - self._POLY_CENTER_REL_INTERVAL_INDEX,
204
+ 0,
205
+ self._num_of_polynomials - 1,
206
+ )
207
+
208
+ @property
209
+ def num_of_polynomials(self):
210
+ return self._num_of_polynomials
@@ -0,0 +1,134 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ ANX Time module
6
+ ---------------
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import functools
12
+
13
+ import numpy as np
14
+ import scipy.optimize
15
+
16
+ from arepytools.geometry.curve_protocols import TwiceDifferentiable3DCurve
17
+
18
+
19
+ def compute_anx_times_core(
20
+ trajectory: TwiceDifferentiable3DCurve,
21
+ time_sampling_step_s: float = 1,
22
+ max_abs_z_error: float = 1e-3,
23
+ max_abs_time_error: float = 1e-6,
24
+ max_search_iterations: float = 100,
25
+ ) -> np.ndarray:
26
+ """Compute ANX times of the specified trajectory.
27
+
28
+ Parameters
29
+ ----------
30
+ trajectory : TwiceDifferentiable3DCurve
31
+ sensor's trajectory from which compute the ANX times
32
+ time_sampling_step_s : float, optional
33
+ sampling step in seconds for the trajectory time axis, by default 1
34
+ max_abs_z_error : float, optional
35
+ maximum absolute error on ANX z coordinate, by default 1e-3
36
+ max_abs_time_error : float, optional
37
+ maximum absolute error on ANX time, by default 1e-6
38
+ max_search_iterations : float, optional
39
+ maximum number of search iterations per ANX, by default 100
40
+
41
+ Returns
42
+ -------
43
+ np.ndarray
44
+ (N,) array of ANX times
45
+ """
46
+
47
+ time_axis_origin = trajectory.domain[0]
48
+ time_axis_rel = np.arange(0, trajectory.domain[1] - trajectory.domain[0], time_sampling_step_s)
49
+ evaluated_positions = trajectory.evaluate(time_axis_rel + time_axis_origin)
50
+
51
+ anx_time_rel_intervals = _find_anx_time_intervals(time_axis_rel=time_axis_rel, positions=evaluated_positions)
52
+
53
+ def get_z_coordinate_bisecting_func(time, origin):
54
+ return trajectory.evaluate(origin + time)[-1]
55
+
56
+ anx_times = []
57
+ for interval in anx_time_rel_intervals:
58
+ central_time = interval[0] + (interval[1] - interval[0]) / 2.0
59
+ velocity_z = trajectory.evaluate_first_derivatives(central_time + time_axis_origin)[-1]
60
+ x_tol = min(max_abs_time_error, max_abs_z_error / abs(velocity_z))
61
+
62
+ get_z_coordinate_bisecting_func = functools.partial(
63
+ get_z_coordinate_bisecting_func, origin=interval[0] + time_axis_origin
64
+ )
65
+
66
+ relative_interval = [t - interval[0] for t in interval]
67
+ anx_time_rel = scipy.optimize.bisect(
68
+ get_z_coordinate_bisecting_func,
69
+ *relative_interval,
70
+ xtol=x_tol,
71
+ maxiter=max_search_iterations,
72
+ )
73
+
74
+ anx_times.append(interval[0] + anx_time_rel)
75
+
76
+ return np.array(anx_times) + time_axis_origin
77
+
78
+
79
+ def compute_relative_times(time_points: np.ndarray, anx_times: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
80
+ """Return the relative time since the greatest node less or equal to absolute time.
81
+
82
+ Parameters
83
+ ----------
84
+ time_points : np.ndarray
85
+ absolute time points, as an array of (N,)
86
+ anx_times : np.ndarray
87
+ sorted absolute anx times, as an array of (N,)
88
+
89
+ Returns
90
+ -------
91
+ tuple[np.ndarray, np.ndarray]
92
+ a tuple of two 1D numpy arrays of relative times and related time_nodes indices,
93
+ nan value and not valid index are returned when previous time node is not available
94
+ """
95
+ relative_times = np.array(
96
+ [(t - anx_times[anx_times <= t][-1] if len(anx_times[anx_times <= t]) > 0 else np.nan) for t in time_points]
97
+ )
98
+ anx_indices = np.array(
99
+ [
100
+ (len(anx_times[anx_times <= t]) - 1 if len(anx_times[anx_times <= t]) > 0 else len(anx_times))
101
+ for t in time_points
102
+ ]
103
+ ).astype(int)
104
+
105
+ return relative_times, anx_indices
106
+
107
+
108
+ def _find_anx_time_intervals(time_axis_rel: np.ndarray, positions: np.ndarray) -> list[tuple[float, float]]:
109
+ """Finding ANX time intervals for the input relative time axis and the corresponding sensor's positions.
110
+
111
+ Parameters
112
+ ----------
113
+ time_axis_rel : np.ndarray
114
+ relative time axis, as a (N,) array of floats
115
+ positions : np.ndarray
116
+ sensor's positions corresponding to the input times, as a (N, 3) array of floats
117
+
118
+ Returns
119
+ -------
120
+ list[tuple[float, float]]
121
+ list of ANX time intervals as (starting time, ending time) of the interval crossing ANX
122
+ """
123
+ anx_intervals = []
124
+ for interval_index in range(time_axis_rel.size - 1):
125
+ interval_begin_index, interval_end_index = interval_index, interval_index + 2
126
+
127
+ z_start, z_stop = positions[interval_begin_index:interval_end_index, -1]
128
+ is_anx_interval = z_start <= 0 < z_stop
129
+
130
+ if is_anx_interval:
131
+ t_start, t_stop = time_axis_rel[interval_begin_index:interval_end_index]
132
+ anx_intervals.append((t_start, t_stop))
133
+
134
+ return anx_intervals
@@ -0,0 +1,88 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Attitude-related utilities
6
+ --------------------------
7
+ """
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ from scipy.spatial import transform
12
+
13
+ from arepytools.geometry.rotation import (
14
+ RotationOrderLike,
15
+ compute_euler_angles_from_rotation,
16
+ compute_rotation,
17
+ )
18
+
19
+
20
+ def compute_antenna_reference_frame_from_euler_angles(
21
+ order: RotationOrderLike,
22
+ initial_reference_frame_axis: np.ndarray,
23
+ yaw: npt.ArrayLike,
24
+ pitch: npt.ArrayLike,
25
+ roll: npt.ArrayLike,
26
+ ) -> np.ndarray:
27
+ """Computing the Antenna Reference Frame (ARF) from euler angles (YAW, PITCH and ROLL) giving a rotation order and
28
+ a reference frame.
29
+
30
+ Parameters
31
+ ----------
32
+ order : RotationOrderLike
33
+ rotation order for the euler angles
34
+ initial_reference_frame_axis : np.ndarray
35
+ reference frame axis of the sensor
36
+ yaw : npt.ArrayLike
37
+ sensor's yaw
38
+ pitch : npt.ArrayLike
39
+ sensor's pitch
40
+ roll : npt.ArrayLike
41
+ sensor's roll
42
+
43
+ Returns
44
+ -------
45
+ np.ndarray
46
+ antenna reference frame for the sensor
47
+ """
48
+
49
+ rotation = compute_rotation(order=order, yaw=yaw, pitch=pitch, roll=roll)
50
+
51
+ return np.matmul(initial_reference_frame_axis, rotation.as_matrix())
52
+
53
+
54
+ def compute_euler_angles_from_antenna_reference_frame(
55
+ initial_reference_frame_axis: np.ndarray,
56
+ antenna_reference_frame: np.ndarray,
57
+ order: RotationOrderLike,
58
+ ) -> np.ndarray:
59
+ """Compute euler angles (YAW, PITCH and ROLL) from Antenna Reference Frame (ARF), the initial reference frame and
60
+ rotation order.
61
+
62
+ Parameters
63
+ ----------
64
+ initial_reference_frame_axis : np.ndarray
65
+ initial reference frame axis of the sensor, (3, 3) or (N, 3, 3)
66
+ antenna_reference_frame : np.ndarray
67
+ antenna reference frame of the sensor, (3, 3) or (N, 3, 3)
68
+ order : RotationOrderLike
69
+ rotation order
70
+
71
+ Returns
72
+ -------
73
+ np.ndarray
74
+ euler angles array, (N, 3), columns being in the same rotation order provided as input
75
+ """
76
+ if initial_reference_frame_axis.shape != antenna_reference_frame.shape:
77
+ raise RuntimeError(
78
+ f"input shape mismatch: init ref frame {initial_reference_frame_axis.shape} != arf "
79
+ + f"{antenna_reference_frame.shape}"
80
+ )
81
+ init_ref_frame = (
82
+ np.transpose(initial_reference_frame_axis, (0, 2, 1))
83
+ if initial_reference_frame_axis.ndim == 3
84
+ else initial_reference_frame_axis.T
85
+ )
86
+ rotation = transform.Rotation.from_matrix(np.matmul(init_ref_frame, antenna_reference_frame))
87
+
88
+ return compute_euler_angles_from_rotation(order=order, rotation=rotation)