pfc-geometry 0.1.8__tar.gz → 0.2.1__tar.gz
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.
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/PKG-INFO +4 -1
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/base.py +49 -10
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/gps.py +14 -28
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/mass.py +10 -1
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/quaternion.py +19 -15
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/transformation.py +9 -1
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/PKG-INFO +4 -1
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/setup.cfg +1 -1
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_base.py +10 -9
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_gps.py +12 -12
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_quaternion.py +11 -13
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_transform.py +6 -5
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/LICENSE +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/README.md +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/__init__.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/coordinate_frame.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/point.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/testing.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/SOURCES.txt +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/dependency_links.txt +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/requires.txt +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/top_level.txt +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/setup.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_coord.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_mass.py +0 -0
- {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_point.py +0 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pfc_geometry
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A package for handling 3D geometry with a nice interface
|
|
5
5
|
Home-page: https://github.com/PyFlightCoach/geometry
|
|
6
6
|
Author: Thomas David
|
|
7
7
|
Author-email: thomasdavid0@gmail.com
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
+
Requires-Dist: setuptools
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
10
13
|
|
|
11
14
|
# geometry #
|
|
12
15
|
|
|
@@ -10,9 +10,10 @@ You should have received a copy of the GNU General Public License along with
|
|
|
10
10
|
this program. If not, see <http://www.gnu.org/licenses/>.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from typing import Type, List
|
|
13
|
+
from typing import Type, List, Self
|
|
14
14
|
from typing_extensions import Self
|
|
15
15
|
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
16
17
|
import pandas as pd
|
|
17
18
|
from numbers import Number
|
|
18
19
|
|
|
@@ -71,14 +72,15 @@ class Base:
|
|
|
71
72
|
self.data = self.__class__._clean_data(np.array(args).T)
|
|
72
73
|
elif all(isinstance(arg, list) for arg in args):
|
|
73
74
|
self.data = self.__class__._clean_data(np.array(args).T)
|
|
74
|
-
|
|
75
|
+
elif all(isinstance(arg, pd.Series) for arg in args):
|
|
76
|
+
self.data = self.__class__._clean_data(np.array(pd.concat(args, axis=1)))
|
|
75
77
|
else:
|
|
76
78
|
raise TypeError
|
|
77
79
|
else:
|
|
78
80
|
raise TypeError(f"Empty {self.__class__.__name__} not allowed")
|
|
79
81
|
|
|
80
82
|
@classmethod
|
|
81
|
-
def _clean_data(cls, data) -> np.
|
|
83
|
+
def _clean_data(cls, data) -> npt.NDArray[np.float64]:
|
|
82
84
|
assert isinstance(data, np.ndarray)
|
|
83
85
|
if data.dtype == 'O':
|
|
84
86
|
raise TypeError(f'data must have homogeneous shape for {cls.__name__}, given {data.shape}')
|
|
@@ -106,7 +108,7 @@ class Base:
|
|
|
106
108
|
def concatenate(cls, items) -> Self:
|
|
107
109
|
return cls(np.concatenate([i.data for i in items], axis=0))
|
|
108
110
|
|
|
109
|
-
def __getattr__(self, name):
|
|
111
|
+
def __getattr__(self, name) -> npt.NDArray[np.float64]:
|
|
110
112
|
if name in self.__class__.cols:
|
|
111
113
|
return self.data[:,self.__class__.cols.index(name)]
|
|
112
114
|
#return res[0] if len(res) == 1 else res
|
|
@@ -153,8 +155,6 @@ class Base:
|
|
|
153
155
|
def degrees(self) -> Self:
|
|
154
156
|
return self.__class__(np.degrees(self.data))
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
158
|
def count(self) -> int:
|
|
159
159
|
return len(self)
|
|
160
160
|
|
|
@@ -197,8 +197,6 @@ class Base:
|
|
|
197
197
|
def __truediv__(self, other) -> Self:
|
|
198
198
|
return self.__class__(self.data / other)
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
200
|
def __str__(self):
|
|
203
201
|
means = ' '.join(f'{c}_={v}' for c, v in zip(self.cols, np.mean(self.data, axis=0).round(2)))
|
|
204
202
|
return f'{self.__class__.__name__}({means}, len={len(self)})'
|
|
@@ -217,10 +215,12 @@ class Base:
|
|
|
217
215
|
return np.einsum('ij,ij->i', self.data, other)
|
|
218
216
|
|
|
219
217
|
def diff(self, dt:np.array) -> Self:
|
|
218
|
+
if not pd.api.types.is_list_like(dt):
|
|
219
|
+
dt = np.full(len(self), dt)
|
|
220
220
|
assert len(dt) == len(self)
|
|
221
221
|
return self.__class__(
|
|
222
222
|
np.gradient(self.data,axis=0) \
|
|
223
|
-
|
|
223
|
+
/ \
|
|
224
224
|
np.tile(dt, (len(self.__class__.cols),1)).T)
|
|
225
225
|
|
|
226
226
|
def to_pandas(self, prefix='', suffix='', columns=None, index=None):
|
|
@@ -279,4 +279,43 @@ class Base:
|
|
|
279
279
|
|
|
280
280
|
def __repr__(self):
|
|
281
281
|
return str(self)
|
|
282
|
-
|
|
282
|
+
|
|
283
|
+
def copy(self):
|
|
284
|
+
return self.__class__(self.data.copy())
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def unwrap(self, discont=np.pi):
|
|
288
|
+
return self.__class__(np.unwrap(self.data, discont=discont, axis=0))
|
|
289
|
+
|
|
290
|
+
def filter(self, order, cutoff, ts: np.ndarray=None):
|
|
291
|
+
from scipy.signal import butter, freqz, filtfilt
|
|
292
|
+
if ts is None:
|
|
293
|
+
ts = np.array(range(len(self)))
|
|
294
|
+
N = len(self)
|
|
295
|
+
T = (ts[-1] - ts[0]) / N
|
|
296
|
+
|
|
297
|
+
fs = 1/T
|
|
298
|
+
b, a = butter(
|
|
299
|
+
order,
|
|
300
|
+
cutoff,
|
|
301
|
+
fs=fs,
|
|
302
|
+
btype='low', analog=False
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return self.__class__(filtfilt(b, a, self.data, axis=0))
|
|
306
|
+
|
|
307
|
+
def fft(self, ts: np.ndarray=None):
|
|
308
|
+
from scipy.fft import fft, fftfreq
|
|
309
|
+
if ts is None:
|
|
310
|
+
ts = np.array(range(len(self)))
|
|
311
|
+
N = len(self)
|
|
312
|
+
T = (ts[-1] - ts[0]) / N
|
|
313
|
+
|
|
314
|
+
yf = fft(self.data, axis=0)
|
|
315
|
+
xf = fftfreq(N, T)[:N//2]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
y=2.0/N * np.abs(yf[0:N//2, :])
|
|
319
|
+
|
|
320
|
+
return pd.DataFrame(np.column_stack([xf, y]), columns=['freq'] + self.cols).set_index('freq')
|
|
321
|
+
|
|
@@ -29,59 +29,52 @@ LOCFAC = math.radians(erad)
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class GPS(Base):
|
|
32
|
-
cols = ["lat", "long"]
|
|
32
|
+
cols = ["lat", "long", "alt"]
|
|
33
33
|
# was 6378137, extra precision removed to match ardupilot
|
|
34
34
|
|
|
35
35
|
def __init__(self, *args, **kwargs):
|
|
36
36
|
super().__init__(*args, **kwargs)
|
|
37
37
|
self._longfac = safecos(self.lat)
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
def offset(self, pin: Point):
|
|
41
|
-
assert len(pin) == len(self)
|
|
42
|
-
latb = self.lat - pin.x / LOCFAC
|
|
43
|
-
|
|
44
|
-
return GPS(
|
|
45
|
-
latb,
|
|
46
|
-
self.long + pin.y / (LOCFAC * safecos(latb))
|
|
47
|
-
)
|
|
48
|
-
|
|
49
39
|
def __eq__(self, other) -> bool:
|
|
50
40
|
return np.all(self.data == other.data)
|
|
51
41
|
|
|
52
42
|
def __sub__(self, other) -> Point:
|
|
53
|
-
assert isinstance(other, GPS)
|
|
43
|
+
assert isinstance(other, GPS), f'Cannot offset a GPS by a {other.__class__.__name__}'
|
|
54
44
|
if len(other) == len(self):
|
|
55
45
|
return Point(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
(self.lat - other.lat) * LOCFAC,
|
|
47
|
+
(self.long - other.long) * LOCFAC * self._longfac,
|
|
48
|
+
other.alt - self.alt
|
|
59
49
|
)
|
|
60
50
|
elif len(other) == 1:
|
|
61
51
|
return self - GPS.full(other, len(self))
|
|
62
52
|
elif len(self) == 1:
|
|
63
53
|
return GPS.full(self, len(self)) - other
|
|
64
54
|
else:
|
|
65
|
-
raise ValueError(f"incompatible lengths for sub ({len(self)})
|
|
55
|
+
raise ValueError(f"incompatible lengths for GPS sub ({len(self)}) != ({len(other)})")
|
|
66
56
|
|
|
67
57
|
def offset(self, pin: Point):
|
|
58
|
+
'''Offset by a point in NED coordinates'''
|
|
68
59
|
if len(pin) == 1 and len(self) > 1:
|
|
69
|
-
pin = Point.full(pin, self
|
|
60
|
+
pin = Point.full(pin, len(self))
|
|
70
61
|
elif len(self) == 1 and len(pin) > 1:
|
|
71
|
-
return
|
|
62
|
+
return GPS.full(self, len(pin)).offset(pin)
|
|
72
63
|
|
|
73
64
|
if not len(pin) == len(self):
|
|
74
|
-
raise ValueError(f"incompatible lengths for offset ({len(self)})
|
|
65
|
+
raise ValueError(f"incompatible lengths for GPS offset ({len(self)}) != ({len(pin)})")
|
|
75
66
|
|
|
76
67
|
latb = self.lat + pin.x / LOCFAC
|
|
77
68
|
return GPS(
|
|
78
69
|
latb,
|
|
79
|
-
self.long + pin.y / (LOCFAC * safecos(latb))
|
|
70
|
+
self.long + pin.y / (LOCFAC * safecos(latb)),
|
|
71
|
+
self.alt - pin.z
|
|
80
72
|
)
|
|
81
73
|
|
|
82
74
|
|
|
83
|
-
|
|
84
75
|
'''
|
|
76
|
+
Extract from ardupilot:
|
|
77
|
+
|
|
85
78
|
// scaling factor from 1e-7 degrees to meters at equator
|
|
86
79
|
// == 1.0e-7 * DEG_TO_RAD * RADIUS_OF_EARTH
|
|
87
80
|
static constexpr float LOCATION_SCALING_FACTOR = 0.011131884502145034f;
|
|
@@ -102,10 +95,3 @@ float Location::long_scale() const
|
|
|
102
95
|
}
|
|
103
96
|
'''
|
|
104
97
|
|
|
105
|
-
|
|
106
|
-
if __name__ == "__main__":
|
|
107
|
-
home = GPS(51.459387, -2.791393)
|
|
108
|
-
|
|
109
|
-
new = GPS(51.458876, -2.789092)
|
|
110
|
-
coord = home - new
|
|
111
|
-
print(coord.x, coord.y)
|
|
@@ -50,5 +50,14 @@ class Mass(Base):
|
|
|
50
50
|
]))
|
|
51
51
|
) + self
|
|
52
52
|
|
|
53
|
+
def momentum(self, v: Point):
|
|
54
|
+
return self.m * v
|
|
55
|
+
|
|
56
|
+
def angular_momentum(self, rvel: Point):
|
|
57
|
+
return Point(
|
|
58
|
+
self.xx * rvel.x + self.xy * rvel.y + self.xz * rvel.z,
|
|
59
|
+
self.yx * rvel.x + self.yy * rvel.y + self.yz * rvel.z,
|
|
60
|
+
self.zx * rvel.x + self.zy * rvel.y + self.zz * rvel.z,
|
|
61
|
+
)
|
|
62
|
+
|
|
53
63
|
|
|
54
|
-
|
|
@@ -15,6 +15,8 @@ from .base import Base
|
|
|
15
15
|
from geometry import PZ
|
|
16
16
|
from typing import Union, Tuple
|
|
17
17
|
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import numpy.typing as npt
|
|
18
20
|
from warnings import warn
|
|
19
21
|
from numbers import Number
|
|
20
22
|
|
|
@@ -44,7 +46,7 @@ class Quaternion(Base):
|
|
|
44
46
|
def inverse(self):
|
|
45
47
|
return self.conjugate().norm()
|
|
46
48
|
|
|
47
|
-
def __mul__(self, other):
|
|
49
|
+
def __mul__(self, other: Union[Number, Quaternion, np.ndarray]) -> Quaternion:
|
|
48
50
|
if isinstance(other, Quaternion):
|
|
49
51
|
a, b = Quaternion.length_check(self, Quaternion.type_check(other))
|
|
50
52
|
w = a.w * b.w - a.axis.dot(b.axis)
|
|
@@ -58,7 +60,7 @@ class Quaternion(Base):
|
|
|
58
60
|
|
|
59
61
|
raise TypeError(f"cant multiply a quaternion by a {other.__class__.__name__}")
|
|
60
62
|
|
|
61
|
-
def __rmul__(self, other):
|
|
63
|
+
def __rmul__(self, other) -> Quaternion:
|
|
62
64
|
#either it should have been picked up by the left hand object or it should commute
|
|
63
65
|
return self * other
|
|
64
66
|
|
|
@@ -72,8 +74,8 @@ class Quaternion(Base):
|
|
|
72
74
|
|
|
73
75
|
@staticmethod
|
|
74
76
|
def from_euler(eul: Point) -> Quaternion:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
'''Create a quaternion from a Point of Euler angles order z, y, x'''
|
|
78
|
+
eul = Point.type_check(eul).unwrap()
|
|
77
79
|
half = eul * 0.5
|
|
78
80
|
c = half.cos
|
|
79
81
|
s = half.sin
|
|
@@ -88,17 +90,15 @@ class Quaternion(Base):
|
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
def to_euler(self) -> Point:
|
|
91
|
-
|
|
92
|
-
# roll (x-axis rotation)
|
|
93
|
+
'''Create a Point of Euler angles order z,y,x'''
|
|
93
94
|
sinr_cosp = 2 * (self.w * self.x + self.y * self.z)
|
|
94
95
|
cosr_cosp = 1 - 2 * (self.x * self.x + self.y * self.y)
|
|
95
96
|
roll = np.arctan2(sinr_cosp, cosr_cosp)
|
|
96
97
|
|
|
97
|
-
# pitch (y-axis rotation)
|
|
98
98
|
sinp = 2 * (self.w * self.y - self.z * self.x)
|
|
99
|
-
|
|
99
|
+
with np.errstate(invalid='ignore'):
|
|
100
|
+
pitch = np.arcsin(sinp)
|
|
100
101
|
|
|
101
|
-
# yaw (z-axis rotation)
|
|
102
102
|
siny_cosp = 2 * (self.w * self.z + self.x * self.y)
|
|
103
103
|
cosy_cosp = 1 - 2 * (self.y * self.y + self.z * self.z)
|
|
104
104
|
yaw = np.arctan2(siny_cosp, cosy_cosp)
|
|
@@ -133,7 +133,7 @@ class Quaternion(Base):
|
|
|
133
133
|
#qdat[abs(Quaternions(qdat)) < .001] = np.array([[1, 0, 0, 0]])
|
|
134
134
|
return Quaternion(qdat)
|
|
135
135
|
|
|
136
|
-
def to_axis_angle(self):
|
|
136
|
+
def to_axis_angle(self) -> Point:
|
|
137
137
|
a = self._to_axis_angle()
|
|
138
138
|
b = (-self)._to_axis_angle()
|
|
139
139
|
|
|
@@ -181,8 +181,10 @@ class Quaternion(Base):
|
|
|
181
181
|
def body_rotate(self, rate: Point) -> Quaternion:
|
|
182
182
|
return (self * Quaternion.from_axis_angle(rate)).norm()
|
|
183
183
|
|
|
184
|
-
def diff(self, dt: np.
|
|
184
|
+
def diff(self, dt: Union[Number, np.ndarray]) -> Point:
|
|
185
185
|
"""differentiate in the world frame"""
|
|
186
|
+
if not pd.api.types.is_list_like(dt):
|
|
187
|
+
dt = np.full(len(self), dt)
|
|
186
188
|
assert len(dt) == len(self)
|
|
187
189
|
dt = dt * len(dt) / (len(dt) - 1)
|
|
188
190
|
|
|
@@ -190,10 +192,12 @@ class Quaternion(Base):
|
|
|
190
192
|
Quaternion(self.data[:-1, :]),
|
|
191
193
|
Quaternion(self.data[1:, :])
|
|
192
194
|
) / dt[:-1]
|
|
193
|
-
return Point(np.vstack([ps.data, ps.data[-1,:]]))
|
|
195
|
+
return Point(np.vstack([ps.data, ps.data[-1,:]]))
|
|
194
196
|
|
|
195
|
-
def body_diff(self, dt: np.
|
|
197
|
+
def body_diff(self, dt: Union[Number, np.ndarray]) -> Point:
|
|
196
198
|
"""differentiate in the body frame"""
|
|
199
|
+
if not pd.api.types.is_list_like(dt):
|
|
200
|
+
dt = np.full(len(self), dt)
|
|
197
201
|
assert len(dt) == len(self)
|
|
198
202
|
dt = dt * len(dt) / (len(dt) - 1)
|
|
199
203
|
|
|
@@ -204,7 +208,7 @@ class Quaternion(Base):
|
|
|
204
208
|
return Point(np.vstack([ps.data, ps.data[-1,:]]))
|
|
205
209
|
|
|
206
210
|
|
|
207
|
-
def to_rotation_matrix(self):
|
|
211
|
+
def to_rotation_matrix(self) -> npt.NDArray[np.float64]:
|
|
208
212
|
"""http://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
|
|
209
213
|
https://github.com/mortlind/pymath3d/blob/master/math3d/quaternion.py
|
|
210
214
|
"""
|
|
@@ -218,7 +222,7 @@ class Quaternion(Base):
|
|
|
218
222
|
]).T
|
|
219
223
|
|
|
220
224
|
@staticmethod
|
|
221
|
-
def from_rotation_matrix(matrix: np.
|
|
225
|
+
def from_rotation_matrix(matrix: npt.NDArray[np.float64]) -> Quaternion:
|
|
222
226
|
# This method assumes row-vector and postmultiplication of that vector
|
|
223
227
|
m = matrix.conj().transpose()
|
|
224
228
|
if m[2, 2] < 0:
|
|
@@ -21,8 +21,16 @@ class Transformation(Base):
|
|
|
21
21
|
def __init__(self, *args, **kwargs):
|
|
22
22
|
if len(args) == len(kwargs) == 0:
|
|
23
23
|
args = np.concatenate([P0().data,Q0().data],axis=1)
|
|
24
|
+
elif len(args) == 1:
|
|
25
|
+
if isinstance(args[0], Point):
|
|
26
|
+
args = np.concatenate([args[0].data,Q0().data],axis=1)
|
|
27
|
+
elif isinstance(args[0], Quaternion):
|
|
28
|
+
args = np.concatenate([P0().data,args[0].data],axis=1)
|
|
24
29
|
if len(args) == 2:
|
|
25
|
-
|
|
30
|
+
_q = args[0] if isinstance(args[0], Quaternion) else args[1]
|
|
31
|
+
_p = args[0] if isinstance(args[0], Point) else args[1]
|
|
32
|
+
assert isinstance(_q, Quaternion) and isinstance(_p, Point)
|
|
33
|
+
args = np.concatenate([_p.data, _q.data], axis=1)
|
|
26
34
|
super().__init__(*args, **kwargs)
|
|
27
35
|
self.p = Point(self.data[:,:3])
|
|
28
36
|
self.q = Quaternion(self.data[:,3:])
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pfc-geometry
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A package for handling 3D geometry with a nice interface
|
|
5
5
|
Home-page: https://github.com/PyFlightCoach/geometry
|
|
6
6
|
Author: Thomas David
|
|
7
7
|
Author-email: thomasdavid0@gmail.com
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
+
Requires-Dist: setuptools
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
10
13
|
|
|
11
14
|
# geometry #
|
|
12
15
|
|
|
@@ -5,7 +5,7 @@ author_email = thomasdavid0@gmail.com
|
|
|
5
5
|
description = A package for handling 3D geometry with a nice interface
|
|
6
6
|
long_description = file: README.md
|
|
7
7
|
long_description_content_type = text/markdown
|
|
8
|
-
version = 0.1
|
|
8
|
+
version = 0.2.1
|
|
9
9
|
url = https://github.com/PyFlightCoach/geometry
|
|
10
10
|
|
|
11
11
|
[options]
|
|
@@ -66,7 +66,7 @@ def test_init_values():
|
|
|
66
66
|
abc = ABC(*[np.ones(10) for _ in range(3)])
|
|
67
67
|
np.testing.assert_array_equal(abc.data, np.ones((10,3)))
|
|
68
68
|
|
|
69
|
-
with raises(
|
|
69
|
+
with raises(ValueError):
|
|
70
70
|
abc = ABC([1,2,3], [1,2], [1,2,3,4])
|
|
71
71
|
|
|
72
72
|
abc = ABC([1,2], [1,2], [1,2])
|
|
@@ -89,10 +89,6 @@ def test_init_kwargs():
|
|
|
89
89
|
with raises(TypeError):
|
|
90
90
|
ABC(ggg=234342)
|
|
91
91
|
|
|
92
|
-
def test_init_list_of_self():
|
|
93
|
-
abc = ABC([ABC(1,2,3), ABC(np.ones((4,3)))])
|
|
94
|
-
np.testing.assert_array_equal(abc.b, np.array([2,1,1,1,1]))
|
|
95
|
-
|
|
96
92
|
def test_init_empty():
|
|
97
93
|
with raises(TypeError):
|
|
98
94
|
ABC()
|
|
@@ -210,7 +206,12 @@ def test_concatenate():
|
|
|
210
206
|
|
|
211
207
|
def test_repr__():
|
|
212
208
|
p = ABC(1,2,3)
|
|
213
|
-
rpr = p.__repr__()
|
|
214
|
-
assert rpr
|
|
215
|
-
|
|
216
|
-
|
|
209
|
+
rpr = p.__repr__()
|
|
210
|
+
assert rpr == "ABC(a_=1.0 b_=2.0 c_=3.0, len=1)"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_three_pandas_series():
|
|
214
|
+
df = pd.DataFrame(np.random.random((10,3)), columns=list("abc"))
|
|
215
|
+
abc = ABC(df.a, df.b, df.c)
|
|
216
|
+
|
|
217
|
+
pd.testing.assert_frame_equal(abc.to_pandas(), df)
|
|
@@ -4,8 +4,8 @@ from geometry.gps import GPS
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
def test_offset():
|
|
7
|
-
c = GPS( 52.542375, -1.631038)
|
|
8
|
-
p = GPS( 52.542264, -1.631817)
|
|
7
|
+
c = GPS( 52.542375, -1.631038, 0)
|
|
8
|
+
p = GPS( 52.542264, -1.631817, 0)
|
|
9
9
|
diff = c - p
|
|
10
10
|
c2 = p.offset(diff)
|
|
11
11
|
|
|
@@ -14,27 +14,27 @@ def test_offset():
|
|
|
14
14
|
np.testing.assert_array_almost_equal(diff.data, diff2.data, 1e-4)
|
|
15
15
|
|
|
16
16
|
def test_diff():
|
|
17
|
-
p0 =
|
|
18
|
-
p0n =
|
|
17
|
+
p0 = GPS( 50.206, 4.1941755999999994, 0)
|
|
18
|
+
p0n = GPS( 50.201, 4.1941755999999994, 0)
|
|
19
19
|
|
|
20
20
|
diff= p0 - p0n # should be south vector
|
|
21
|
-
|
|
21
|
+
approx(diff.y, 0)
|
|
22
22
|
assert diff.x > 0
|
|
23
23
|
|
|
24
|
-
p0e=
|
|
24
|
+
p0e= GPS( 50.206, 4.195, 0)
|
|
25
25
|
diff= p0 - p0e # should be west vector
|
|
26
|
-
|
|
26
|
+
approx(diff.x, 0)
|
|
27
27
|
assert diff.y < 0
|
|
28
28
|
|
|
29
29
|
def test_diff():
|
|
30
|
-
p0 = GPS( 50.201, 4.195)
|
|
31
|
-
p0n = GPS( 50.206, 4.195) #directly north of p0
|
|
30
|
+
p0 = GPS( 50.201, 4.195, 0)
|
|
31
|
+
p0n = GPS( 50.206, 4.195, 0) #directly north of p0
|
|
32
32
|
|
|
33
33
|
diff= p0 - p0n # should be south vector
|
|
34
34
|
assert diff.y == approx(0)
|
|
35
35
|
assert diff.x < 0
|
|
36
36
|
|
|
37
|
-
p0e= GPS( 50.201, 4.196)
|
|
37
|
+
p0e= GPS( 50.201, 4.196, 0)
|
|
38
38
|
diff= p0 - p0e # should be west vector
|
|
39
39
|
assert diff.x == approx(0)
|
|
40
40
|
assert diff.y < 0
|
|
@@ -43,8 +43,8 @@ def test_diff():
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def test_sub():
|
|
46
|
-
centre = GPS( 52.542375, -1.631038)
|
|
47
|
-
pilot = GPS( 52.542264, -1.631817)
|
|
46
|
+
centre = GPS( 52.542375, -1.631038, 0)
|
|
47
|
+
pilot = GPS( 52.542264, -1.631817, 0)
|
|
48
48
|
|
|
49
49
|
vec = centre - pilot
|
|
50
50
|
|
|
@@ -3,8 +3,10 @@ from pytest import mark, approx, raises
|
|
|
3
3
|
from geometry.quaternion import Quaternion, Q0
|
|
4
4
|
from geometry.point import Point, PX, PY, PZ, P0
|
|
5
5
|
from geometry import Euler, Euldeg
|
|
6
|
+
from geometry.testing import assert_almost_equal
|
|
6
7
|
import pandas as pd
|
|
7
8
|
import numpy as np
|
|
9
|
+
import quaternion
|
|
8
10
|
from scipy.spatial.transform import Rotation as R
|
|
9
11
|
|
|
10
12
|
|
|
@@ -44,6 +46,13 @@ def test_from_euler():
|
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
def test_from_euler_unwrap():
|
|
50
|
+
q = Q0(10).rotate(PZ()*np.radians(np.linspace(0,360, 10)))
|
|
51
|
+
np.testing.assert_array_almost_equal(
|
|
52
|
+
Quaternion.from_euler(q.to_euler()).diff(1).data,
|
|
53
|
+
q.diff(1).data
|
|
54
|
+
)
|
|
55
|
+
|
|
47
56
|
|
|
48
57
|
def test_to_euler():
|
|
49
58
|
qarr = Quaternion(np.random.random((2, 4))).norm()
|
|
@@ -111,18 +120,6 @@ def test_body_diff():
|
|
|
111
120
|
Point.X(np.pi/100, 100).data
|
|
112
121
|
)
|
|
113
122
|
|
|
114
|
-
def test_transform_point2():
|
|
115
|
-
tqs = Quaternion(np.random.random((10,4))).norm()
|
|
116
|
-
|
|
117
|
-
np.testing.assert_array_almost_equal(
|
|
118
|
-
tqs.transform_point(Point(1, 1, 1)).data,
|
|
119
|
-
Quaternion.rotate_vectors(
|
|
120
|
-
np.array([np.quaternion(*q.data[0]) for q in tqs]),
|
|
121
|
-
Point(1,1,1).tile(1).data,
|
|
122
|
-
axis=1
|
|
123
|
-
).reshape(10,3)
|
|
124
|
-
)
|
|
125
|
-
|
|
126
123
|
def test_tp_2():
|
|
127
124
|
np.testing.assert_array_equal(
|
|
128
125
|
Quaternion(0.1, 0, 0,0).norm().transform_point(Point(1,0,0)).data,
|
|
@@ -162,7 +159,8 @@ def test_to_from_axis_angle():
|
|
|
162
159
|
|
|
163
160
|
def test_to_axis_angle():
|
|
164
161
|
q1 = Quaternion.from_euler(PZ(np.pi/4))
|
|
165
|
-
|
|
162
|
+
|
|
163
|
+
assert_almost_equal( q1.to_axis_angle(), PZ(np.pi/4))
|
|
166
164
|
|
|
167
165
|
|
|
168
166
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pytest import mark, approx
|
|
2
2
|
from geometry import Coord, Transformation, Point, Quaternion, PX, PY, PZ, P0
|
|
3
3
|
import numpy as np
|
|
4
|
-
|
|
4
|
+
from geometry.testing import assert_almost_equal
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_from_coords():
|
|
@@ -12,13 +12,14 @@ def test_from_coords():
|
|
|
12
12
|
|
|
13
13
|
ps = Point(np.random.random((100, 3)))
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
ps
|
|
17
|
-
trans_from.translate(trans_to.translate(ps))
|
|
15
|
+
assert_almost_equal(
|
|
16
|
+
ps,
|
|
17
|
+
trans_from.translate(trans_to.translate(ps))
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
qs = Quaternion.from_euler(ps)
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
assert_almost_equal(
|
|
22
23
|
qs,
|
|
23
24
|
trans_from.rotate(trans_to.rotate(qs))
|
|
24
25
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|