pyelq 1.1.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.
- pyelq/__init__.py +19 -0
- pyelq/component/__init__.py +6 -0
- pyelq/component/background.py +391 -0
- pyelq/component/component.py +79 -0
- pyelq/component/error_model.py +327 -0
- pyelq/component/offset.py +183 -0
- pyelq/component/source_model.py +875 -0
- pyelq/coordinate_system.py +598 -0
- pyelq/data_access/__init__.py +5 -0
- pyelq/data_access/data_access.py +104 -0
- pyelq/dispersion_model/__init__.py +5 -0
- pyelq/dispersion_model/gaussian_plume.py +625 -0
- pyelq/dlm.py +497 -0
- pyelq/gas_species.py +232 -0
- pyelq/meteorology.py +387 -0
- pyelq/model.py +209 -0
- pyelq/plotting/__init__.py +5 -0
- pyelq/plotting/plot.py +1183 -0
- pyelq/preprocessing.py +262 -0
- pyelq/sensor/__init__.py +5 -0
- pyelq/sensor/beam.py +55 -0
- pyelq/sensor/satellite.py +59 -0
- pyelq/sensor/sensor.py +241 -0
- pyelq/source_map.py +115 -0
- pyelq/support_functions/__init__.py +5 -0
- pyelq/support_functions/post_processing.py +377 -0
- pyelq/support_functions/spatio_temporal_interpolation.py +229 -0
- pyelq-1.1.0.dist-info/LICENSE.md +202 -0
- pyelq-1.1.0.dist-info/LICENSES/Apache-2.0.txt +73 -0
- pyelq-1.1.0.dist-info/METADATA +127 -0
- pyelq-1.1.0.dist-info/RECORD +32 -0
- pyelq-1.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
# -*- coding: utf-8 -*-
|
|
6
|
+
"""Coordinate System.
|
|
7
|
+
|
|
8
|
+
This code provides the definition of, and the functionality for, all the main coordinate systems that are used in
|
|
9
|
+
pyELQ. Each coordinate system has relevant methods for features that are commonly required. Also provided is a set of
|
|
10
|
+
conversions between each of the systems, alongside some functionality for interpolation.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pymap3d as pm
|
|
20
|
+
from scipy.spatial import KDTree
|
|
21
|
+
from scipy.stats import qmc
|
|
22
|
+
|
|
23
|
+
import pyelq.support_functions.spatio_temporal_interpolation as sti
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_latin_hypercube(bounds: np.ndarray, nof_samples: int) -> np.ndarray:
|
|
27
|
+
"""Latin Hypercube samples.
|
|
28
|
+
|
|
29
|
+
Draw samples according to a Latin Hypercube design within the specified bounds.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
bounds (np.ndarray): Limits of the resulting hypercube of size [dim x 2]
|
|
33
|
+
nof_samples (int): Number of samples to draw
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
array (np.ndarray): Samples forming the Latin Hypercube
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
dimension = bounds.shape[0]
|
|
40
|
+
sampler = qmc.LatinHypercube(d=dimension)
|
|
41
|
+
sample = sampler.random(n=nof_samples)
|
|
42
|
+
array = qmc.scale(sample, np.min(bounds, axis=1), np.max(bounds, axis=1))
|
|
43
|
+
return array
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Coordinate(ABC):
|
|
48
|
+
"""Abstract base class for coordinate transformations.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
use_degrees (bool): Flag if reference uses degrees (True) or radians (False). Defaults to True.
|
|
52
|
+
ellipsoid (pm.Ellipsoid): Definition of the Ellipsoid used in the coordinate system, for which the default is
|
|
53
|
+
WGS84. See: https://en.wikipedia.org/wiki/World_Geodetic_System.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
use_degrees: bool = field(init=False)
|
|
58
|
+
ellipsoid: pm.Ellipsoid = field(init=False)
|
|
59
|
+
|
|
60
|
+
def __post_init__(self):
|
|
61
|
+
self.use_degrees = True
|
|
62
|
+
self.ellipsoid = pm.Ellipsoid.from_name("wgs84")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def nof_observations(self) -> int:
|
|
67
|
+
"""Number of observations contained in the class instance, implemented as dependent property."""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def from_array(self, array: np.ndarray) -> None:
|
|
71
|
+
"""Unstack a numpy array into the corresponding coordinates.
|
|
72
|
+
|
|
73
|
+
The method has no return as it sets the corresponding attributes of the coordinate class instance.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
array (np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single
|
|
77
|
+
array
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def to_array(self, dim: int = 3) -> np.ndarray:
|
|
83
|
+
"""Stacks coordinates together into a numpy array.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
dim (int, optional): Number of dimensions to use, which is either 2 or 3.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
np.ndarray: Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single array
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def to_lla(self):
|
|
95
|
+
"""LLA: Converts coordinates to latitude/longitude/altitude system."""
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def to_ecef(self):
|
|
99
|
+
"""ECEF: Convert coordinates to earth centered earth fixed coordinates."""
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def to_enu(self, ref_latitude: float = None, ref_longitude: float = None, ref_altitude: float = None):
|
|
103
|
+
"""Converts coordinates to East North Up system.
|
|
104
|
+
|
|
105
|
+
If a reference is not provided, the minimum of coordinates in Lat/Lon/Alt is used as the reference.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
ref_latitude (float, optional): reference latitude for ENU
|
|
109
|
+
ref_longitude (float, optional): reference longitude for ENU
|
|
110
|
+
ref_altitude (float, optional): reference altitude for ENU
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
(ENU): East North Up coordinate object
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def to_object_type(self, coordinate_object):
|
|
118
|
+
"""Converts current object to same class as input coordinate_object.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
coordinate_object (Coordinate): An coordinate object which provides the coordinate system to convert self to
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
(Coordinate): The converted coordinate object
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
if type(coordinate_object) is not type(self):
|
|
128
|
+
if isinstance(coordinate_object, LLA):
|
|
129
|
+
temp_object = self.to_lla()
|
|
130
|
+
elif isinstance(coordinate_object, ENU):
|
|
131
|
+
temp_object = self.to_enu(
|
|
132
|
+
ref_latitude=coordinate_object.ref_latitude,
|
|
133
|
+
ref_longitude=coordinate_object.ref_longitude,
|
|
134
|
+
ref_altitude=coordinate_object.ref_altitude,
|
|
135
|
+
)
|
|
136
|
+
elif isinstance(coordinate_object, ECEF):
|
|
137
|
+
temp_object = self.to_ecef()
|
|
138
|
+
else:
|
|
139
|
+
raise TypeError("Please provide a valid coordinate type")
|
|
140
|
+
|
|
141
|
+
return temp_object
|
|
142
|
+
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def interpolate(self, values: np.ndarray, locations, dim: int = 3, **kwargs) -> np.ndarray:
|
|
146
|
+
"""Interpolate data using coordinate object.
|
|
147
|
+
|
|
148
|
+
If locations coordinate system does not match self's coordinate system it will be converted to same type as
|
|
149
|
+
self. In the ENU case extra checking needs to take place to check reference locations match up.
|
|
150
|
+
|
|
151
|
+
If only 1 value is provided which needs to be interpolated to many other locations we just set the value at all
|
|
152
|
+
these locations to the single input value
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
values (np.ndarray): Values to interpolate, consistent with location in self
|
|
156
|
+
locations (Coordinate): Coordinate object containing locations to which you want to interpolate
|
|
157
|
+
dim (int): Number of dimensions to use for interpolation (2 or 3)
|
|
158
|
+
**kwargs (dict): Other arguments available in scipy.interpolate.griddata e.g. method, fill_value
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Result (np.ndarray): Interpolated values at requested locations.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
locations = locations.to_object_type(coordinate_object=self)
|
|
165
|
+
|
|
166
|
+
if isinstance(self, ENU):
|
|
167
|
+
if (
|
|
168
|
+
self.ref_latitude != locations.ref_latitude
|
|
169
|
+
or self.ref_longitude != locations.ref_longitude
|
|
170
|
+
or self.ref_altitude != locations.ref_altitude
|
|
171
|
+
):
|
|
172
|
+
locations = locations.to_lla()
|
|
173
|
+
locations = locations.to_enu(
|
|
174
|
+
ref_latitude=self.ref_latitude, ref_longitude=self.ref_longitude, ref_altitude=self.ref_altitude
|
|
175
|
+
)
|
|
176
|
+
result = sti.interpolate(
|
|
177
|
+
location_in=self.to_array(dim),
|
|
178
|
+
values_in=values.flatten(),
|
|
179
|
+
location_out=locations.to_array(dim=dim),
|
|
180
|
+
**kwargs,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
def make_grid(
|
|
186
|
+
self, bounds: np.ndarray, grid_type: str = "rectangular", shape: Union[tuple, np.ndarray] = (5, 5, 1)
|
|
187
|
+
) -> np.ndarray:
|
|
188
|
+
"""Generates grid of values locations based on specified inputs.
|
|
189
|
+
|
|
190
|
+
If the grid type is 'spherical', we scale the latitude and longitude from -90/90 and -180/180 to 0/1 for the
|
|
191
|
+
use in temp_lat_rad and temp_lon_rad.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
bounds (np.ndarray): Limits of the grid on which to generate the grid of size [dim x 2]
|
|
195
|
+
if dim == 2 we assume the third dimension will be zeros
|
|
196
|
+
grid_type (str, optional): Type of grid to generate, default 'rectangular':
|
|
197
|
+
rectangular == rectangular grid of shape grd_shape,
|
|
198
|
+
spherical == grid of shape grid_shape taking into account a spherical spacing
|
|
199
|
+
shape: (tuple, optional): Number of grid cells to generate in each dimension, total number of
|
|
200
|
+
grid cells will be the product of the entries of this tuple
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
np.ndarray: gridded of locations
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
dimension = bounds.shape[0]
|
|
207
|
+
|
|
208
|
+
if grid_type == "rectangular":
|
|
209
|
+
dim_0 = np.linspace(bounds[0, 0], bounds[0, 1], num=shape[0])
|
|
210
|
+
dim_1 = np.linspace(bounds[1, 0], bounds[1, 1], num=shape[1])
|
|
211
|
+
if dimension == 3:
|
|
212
|
+
dim_2 = np.linspace(bounds[2, 0], bounds[2, 1], num=shape[2])
|
|
213
|
+
else:
|
|
214
|
+
dim_2 = np.array(0)
|
|
215
|
+
|
|
216
|
+
dim_0, dim_1, dim_2 = np.meshgrid(dim_0, dim_1, dim_2)
|
|
217
|
+
array = np.stack([dim_0.flatten(), dim_1.flatten(), dim_2.flatten()], axis=1)
|
|
218
|
+
elif grid_type == "spherical":
|
|
219
|
+
temp_object = deepcopy(self)
|
|
220
|
+
temp_object.from_array(array=bounds)
|
|
221
|
+
temp_object = temp_object.to_lla()
|
|
222
|
+
temp_object.latitude = (temp_object.latitude - (-90)) / 180
|
|
223
|
+
temp_object.longitude = (temp_object.longitude - (-180)) / 360
|
|
224
|
+
|
|
225
|
+
temp_lat_rad = np.linspace(start=temp_object.latitude[0], stop=temp_object.latitude[1], num=shape[0])
|
|
226
|
+
temp_lon_rad = np.linspace(start=temp_object.longitude[0], stop=temp_object.longitude[1], num=shape[1])
|
|
227
|
+
|
|
228
|
+
longitude = (2 * np.pi * temp_lon_rad - np.pi) * 180 / np.pi
|
|
229
|
+
latitude = (np.arccos(1 - 2 * temp_lat_rad) - 0.5 * np.pi) * 180 / np.pi
|
|
230
|
+
if dimension == 3:
|
|
231
|
+
altitude = np.linspace(start=temp_object.altitude[0], stop=temp_object.altitude[1], num=shape[2])
|
|
232
|
+
latitude, longitude, altitude = np.meshgrid(latitude, longitude, altitude)
|
|
233
|
+
array = np.stack(
|
|
234
|
+
[latitude.flatten() * np.pi / 180, longitude.flatten() * np.pi / 180, altitude.flatten()], axis=1
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
latitude, longitude = np.meshgrid(latitude, longitude)
|
|
238
|
+
array = np.stack([latitude.flatten() * np.pi / 180, longitude.flatten() * np.pi / 180], axis=1)
|
|
239
|
+
|
|
240
|
+
temp_object.from_array(array=array)
|
|
241
|
+
temp_object = temp_object.to_object_type(self)
|
|
242
|
+
array = temp_object.to_array()
|
|
243
|
+
else:
|
|
244
|
+
raise NotImplementedError("Please provide a valid grid type")
|
|
245
|
+
|
|
246
|
+
return array
|
|
247
|
+
|
|
248
|
+
def create_tree(self) -> KDTree:
|
|
249
|
+
"""Create KD tree for the purpose of fast distance computation.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
KDTree: Spatial KD tree
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
return KDTree(self.to_array())
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass
|
|
259
|
+
class LLA(Coordinate):
|
|
260
|
+
"""Defines the properties and functionality of the latitude/ longitude/ altitude coordinate system.
|
|
261
|
+
|
|
262
|
+
Attributes:
|
|
263
|
+
latitude (np.ndarray): Latitude values in degrees.
|
|
264
|
+
longitude (np.ndarray): Longitude values in degrees.
|
|
265
|
+
altitude (np.ndarray): Altitude values in meters with respect to a spheroid.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
latitude: np.ndarray = None
|
|
270
|
+
longitude: np.ndarray = None
|
|
271
|
+
altitude: np.ndarray = None
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def nof_observations(self):
|
|
275
|
+
"""Number of observations contained in the class instance, implemented as dependent property."""
|
|
276
|
+
if self.latitude is None:
|
|
277
|
+
return 0
|
|
278
|
+
return self.latitude.size
|
|
279
|
+
|
|
280
|
+
def from_array(self, array):
|
|
281
|
+
"""Unstack a numpy array into the corresponding coordinates.
|
|
282
|
+
|
|
283
|
+
The method has no return as it sets the corresponding attributes of the coordinate class instance.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
array (np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single
|
|
287
|
+
array
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
dim = array.shape[1]
|
|
291
|
+
self.latitude = array[:, 0]
|
|
292
|
+
self.longitude = array[:, 1]
|
|
293
|
+
self.altitude = np.zeros_like(self.latitude)
|
|
294
|
+
if dim == 3:
|
|
295
|
+
self.altitude = array[:, 2]
|
|
296
|
+
|
|
297
|
+
def to_array(self, dim=3):
|
|
298
|
+
"""Stacks coordinates together into a numpy array.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
dim (int, optional): Number of dimensions to use, which is either 2 or 3.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
(np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single array
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
if dim == 2:
|
|
308
|
+
return np.stack((self.latitude.flatten(), self.longitude.flatten()), axis=1)
|
|
309
|
+
return np.stack((self.latitude.flatten(), self.longitude.flatten(), self.altitude.flatten()), axis=1)
|
|
310
|
+
|
|
311
|
+
def to_lla(self):
|
|
312
|
+
"""LLA: Converts coordinates to latitude/longitude/altitude system."""
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
def to_ecef(self):
|
|
316
|
+
"""ECEF: Convert coordinates to earth centered earth fixed coordinates."""
|
|
317
|
+
if self.altitude is None:
|
|
318
|
+
self.altitude = np.zeros(self.latitude.shape)
|
|
319
|
+
ecef_object = ECEF()
|
|
320
|
+
ecef_object.x, ecef_object.y, ecef_object.z = pm.geodetic2ecef(
|
|
321
|
+
lat=self.latitude, lon=self.longitude, alt=self.altitude, ell=self.ellipsoid, deg=self.use_degrees
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return ecef_object
|
|
325
|
+
|
|
326
|
+
def to_enu(self, ref_latitude=None, ref_longitude=None, ref_altitude=None):
|
|
327
|
+
"""Converts coordinates to East North Up system.
|
|
328
|
+
|
|
329
|
+
If a reference is not provided, the minimum of coordinates in Lat/Lon/Alt is used as the reference.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
ref_latitude (float, optional): reference latitude for ENU
|
|
333
|
+
ref_longitude (float, optional): reference longitude for ENU
|
|
334
|
+
ref_altitude (float, optional): reference altitude for ENU
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
(ENU): East North Up coordinate object
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
if self.altitude is None:
|
|
341
|
+
self.altitude = np.zeros(self.latitude.shape)
|
|
342
|
+
|
|
343
|
+
if ref_altitude is None:
|
|
344
|
+
ref_altitude = np.amin(self.altitude)
|
|
345
|
+
|
|
346
|
+
if ref_latitude is None:
|
|
347
|
+
ref_latitude = np.amin(self.latitude)
|
|
348
|
+
|
|
349
|
+
if ref_longitude is None:
|
|
350
|
+
ref_longitude = np.amin(self.longitude)
|
|
351
|
+
|
|
352
|
+
enu_object = ENU(ref_latitude=ref_latitude, ref_longitude=ref_longitude, ref_altitude=ref_altitude)
|
|
353
|
+
|
|
354
|
+
enu_object.east, enu_object.north, enu_object.up = pm.geodetic2enu(
|
|
355
|
+
lat=self.latitude,
|
|
356
|
+
lon=self.longitude,
|
|
357
|
+
h=self.altitude,
|
|
358
|
+
lat0=ref_latitude,
|
|
359
|
+
lon0=ref_longitude,
|
|
360
|
+
h0=ref_altitude,
|
|
361
|
+
ell=self.ellipsoid,
|
|
362
|
+
deg=self.use_degrees,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return enu_object
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@dataclass
|
|
369
|
+
class ENU(Coordinate):
|
|
370
|
+
"""Defines the properties and functionality of a local East-North-Up coordinate system.
|
|
371
|
+
|
|
372
|
+
Positions relative to some reference location in metres.
|
|
373
|
+
|
|
374
|
+
Attributes:
|
|
375
|
+
ref_latitude (float): Reference latitude for current ENU system.
|
|
376
|
+
ref_longitude (float): Reference longitude for current ENU system.
|
|
377
|
+
ref_altitude (float): Reference altitude for current ENU system.
|
|
378
|
+
east (np.ndarray): East values.
|
|
379
|
+
north (np.ndarray): North values.
|
|
380
|
+
up: (np.ndarray): Up values.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
ref_latitude: float
|
|
385
|
+
ref_longitude: float
|
|
386
|
+
ref_altitude: float
|
|
387
|
+
east: np.ndarray = None
|
|
388
|
+
north: np.ndarray = None
|
|
389
|
+
up: np.ndarray = None
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def nof_observations(self):
|
|
393
|
+
"""Number of observations contained in the class instance, implemented as dependent property."""
|
|
394
|
+
if self.east is None:
|
|
395
|
+
return 0
|
|
396
|
+
return self.east.size
|
|
397
|
+
|
|
398
|
+
def from_array(self, array):
|
|
399
|
+
"""Unstack a numpy array into the corresponding coordinates.
|
|
400
|
+
|
|
401
|
+
The method has no return as it sets the corresponding attributes of the coordinate class instance.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
array (np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single
|
|
405
|
+
array
|
|
406
|
+
|
|
407
|
+
"""
|
|
408
|
+
dim = array.shape[1]
|
|
409
|
+
self.east = array[:, 0]
|
|
410
|
+
self.north = array[:, 1]
|
|
411
|
+
self.up = np.zeros_like(self.east)
|
|
412
|
+
if dim == 3:
|
|
413
|
+
self.up = array[:, 2]
|
|
414
|
+
|
|
415
|
+
def to_array(self, dim=3):
|
|
416
|
+
"""Stacks coordinates together into a numpy array.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
dim (int, optional): Number of dimensions to use, which is either 2 or 3.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
(np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single array
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
if dim == 2:
|
|
426
|
+
return np.stack((self.east.flatten(), self.north.flatten()), axis=1)
|
|
427
|
+
return np.stack((self.east.flatten(), self.north.flatten(), self.up.flatten()), axis=1)
|
|
428
|
+
|
|
429
|
+
def to_enu(self, ref_latitude=None, ref_longitude=None, ref_altitude=None):
|
|
430
|
+
"""Converts coordinates to East North Up system.
|
|
431
|
+
|
|
432
|
+
If a reference is not provided, the minimum of coordinates in Lat/Lon/Alt is used as the reference.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
ref_latitude (float, optional): reference latitude for ENU
|
|
436
|
+
ref_longitude (float, optional): reference longitude for ENU
|
|
437
|
+
ref_altitude (float, optional): reference altitude for ENU
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
(ENU): East North Up coordinate object
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
if ref_latitude is None:
|
|
444
|
+
ref_latitude = self.ref_latitude
|
|
445
|
+
|
|
446
|
+
if ref_longitude is None:
|
|
447
|
+
ref_longitude = self.ref_longitude
|
|
448
|
+
|
|
449
|
+
if ref_altitude is None:
|
|
450
|
+
ref_altitude = self.ref_altitude
|
|
451
|
+
|
|
452
|
+
if (
|
|
453
|
+
self.ref_latitude == ref_latitude
|
|
454
|
+
and self.ref_longitude == ref_longitude
|
|
455
|
+
and self.ref_altitude == ref_altitude
|
|
456
|
+
):
|
|
457
|
+
return self
|
|
458
|
+
|
|
459
|
+
ecef_temp = self.to_ecef()
|
|
460
|
+
|
|
461
|
+
return ecef_temp.to_enu(ref_longitude=ref_longitude, ref_latitude=ref_latitude, ref_altitude=ref_altitude)
|
|
462
|
+
|
|
463
|
+
def to_lla(self):
|
|
464
|
+
"""LLA: Converts coordinates to latitude/longitude/altitude system."""
|
|
465
|
+
lla_object = LLA()
|
|
466
|
+
|
|
467
|
+
lla_object.latitude, lla_object.longitude, lla_object.altitude = pm.enu2geodetic(
|
|
468
|
+
e=self.east,
|
|
469
|
+
n=self.north,
|
|
470
|
+
u=self.up,
|
|
471
|
+
lat0=self.ref_latitude,
|
|
472
|
+
lon0=self.ref_longitude,
|
|
473
|
+
h0=self.ref_altitude,
|
|
474
|
+
ell=self.ellipsoid,
|
|
475
|
+
deg=self.use_degrees,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return lla_object
|
|
479
|
+
|
|
480
|
+
def to_ecef(self):
|
|
481
|
+
"""ECEF: Convert coordinates to earth centered earth fixed coordinates."""
|
|
482
|
+
ecef_object = ECEF()
|
|
483
|
+
|
|
484
|
+
ecef_object.x, ecef_object.y, ecef_object.z = pm.enu2ecef(
|
|
485
|
+
e1=self.east,
|
|
486
|
+
n1=self.north,
|
|
487
|
+
u1=self.up,
|
|
488
|
+
lat0=self.ref_latitude,
|
|
489
|
+
lon0=self.ref_longitude,
|
|
490
|
+
h0=self.ref_altitude,
|
|
491
|
+
ell=self.ellipsoid,
|
|
492
|
+
deg=self.use_degrees,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return ecef_object
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@dataclass
|
|
499
|
+
class ECEF(Coordinate):
|
|
500
|
+
"""Defines the properties and functionality of an Earth-Centered, Earth-Fixed coordinate system.
|
|
501
|
+
|
|
502
|
+
See: https://en.wikipedia.org/wiki/Earth-centered,_Earth-fixed_coordinate_system
|
|
503
|
+
|
|
504
|
+
Attributes:
|
|
505
|
+
x (np.ndarray): Eastings values [metres]
|
|
506
|
+
y (np.ndarray): Northings values [metres]
|
|
507
|
+
z (np.ndarray): Altitude values [metres]
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
x: np.ndarray = None
|
|
512
|
+
y: np.ndarray = None
|
|
513
|
+
z: np.ndarray = None
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def nof_observations(self):
|
|
517
|
+
"""Number of observations contained in the class instance, implemented as dependent property."""
|
|
518
|
+
if self.x is None:
|
|
519
|
+
return 0
|
|
520
|
+
return self.x.size
|
|
521
|
+
|
|
522
|
+
def from_array(self, array):
|
|
523
|
+
"""Unstack a numpy array into the corresponding coordinates.
|
|
524
|
+
|
|
525
|
+
The method has no return as it sets the corresponding attributes of the coordinate class instance.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
array (np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single
|
|
529
|
+
array
|
|
530
|
+
|
|
531
|
+
"""
|
|
532
|
+
dim = array.shape[1]
|
|
533
|
+
self.x = array[:, 0]
|
|
534
|
+
self.y = array[:, 1]
|
|
535
|
+
self.z = np.zeros_like(self.x)
|
|
536
|
+
if dim == 3:
|
|
537
|
+
self.z = array[:, 2]
|
|
538
|
+
|
|
539
|
+
def to_array(self, dim=3):
|
|
540
|
+
"""Stacks coordinates together into a numpy array.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
dim (int, optional): Number of dimensions to use, which is either 2 or 3.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
(np.ndarray): Numpy array of size [n x dim] with n>0 containing the coordinates stacked into a single array
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
if dim == 2:
|
|
550
|
+
return np.stack((self.x.flatten(), self.y.flatten()), axis=1)
|
|
551
|
+
return np.stack((self.x.flatten(), self.y.flatten(), self.z.flatten()), axis=1)
|
|
552
|
+
|
|
553
|
+
def to_ecef(self):
|
|
554
|
+
"""ECEF: Convert coordinates to earth centered earth fixed coordinates."""
|
|
555
|
+
return self
|
|
556
|
+
|
|
557
|
+
def to_lla(self):
|
|
558
|
+
"""LLA: Converts coordinates to latitude/longitude/altitude system."""
|
|
559
|
+
lla_object = LLA()
|
|
560
|
+
|
|
561
|
+
lla_object.latitude, lla_object.longitude, lla_object.altitude = pm.ecef2geodetic(
|
|
562
|
+
self.x, self.y, self.z, ell=self.ellipsoid, deg=self.use_degrees
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
return lla_object
|
|
566
|
+
|
|
567
|
+
def to_enu(self, ref_latitude=None, ref_longitude=None, ref_altitude=None):
|
|
568
|
+
"""Converts coordinates to East North Up system.
|
|
569
|
+
|
|
570
|
+
If a reference is not provided, the minimum of coordinates in Lat/Lon/Alt is used as the reference.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
ref_latitude (float, optional): reference latitude for ENU
|
|
574
|
+
ref_longitude (float, optional): reference longitude for ENU
|
|
575
|
+
ref_altitude (float, optional): reference altitude for ENU
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
(ENU): East North Up coordinate object
|
|
579
|
+
|
|
580
|
+
"""
|
|
581
|
+
if ref_latitude is None or ref_longitude is None or ref_altitude is None:
|
|
582
|
+
lla_object = self.to_lla()
|
|
583
|
+
return lla_object.to_enu()
|
|
584
|
+
|
|
585
|
+
enu_object = ENU(ref_latitude=ref_latitude, ref_longitude=ref_longitude, ref_altitude=ref_altitude)
|
|
586
|
+
|
|
587
|
+
enu_object.east, enu_object.north, enu_object.up = pm.ecef2enu(
|
|
588
|
+
x=self.x,
|
|
589
|
+
y=self.y,
|
|
590
|
+
z=self.z,
|
|
591
|
+
lat0=ref_latitude,
|
|
592
|
+
lon0=ref_longitude,
|
|
593
|
+
h0=ref_altitude,
|
|
594
|
+
ell=self.ellipsoid,
|
|
595
|
+
deg=self.use_degrees,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return enu_object
|