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.
Files changed (26) hide show
  1. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/PKG-INFO +4 -1
  2. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/base.py +49 -10
  3. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/gps.py +14 -28
  4. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/mass.py +10 -1
  5. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/quaternion.py +19 -15
  6. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/transformation.py +9 -1
  7. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/PKG-INFO +4 -1
  8. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/setup.cfg +1 -1
  9. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_base.py +10 -9
  10. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_gps.py +12 -12
  11. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_quaternion.py +11 -13
  12. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_transform.py +6 -5
  13. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/LICENSE +0 -0
  14. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/README.md +0 -0
  15. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/__init__.py +0 -0
  16. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/coordinate_frame.py +0 -0
  17. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/point.py +0 -0
  18. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/geometry/testing.py +0 -0
  19. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/SOURCES.txt +0 -0
  20. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/dependency_links.txt +0 -0
  21. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/requires.txt +0 -0
  22. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/pfc_geometry.egg-info/top_level.txt +0 -0
  23. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/setup.py +0 -0
  24. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_coord.py +0 -0
  25. {pfc_geometry-0.1.8 → pfc_geometry-0.2.1}/tests/test_mass.py +0 -0
  26. {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.8
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.ndarray:
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
- -(other.lat - self.lat) * LOCFAC,
57
- -(other.long - self.long) * LOCFAC * self._longfac,
58
- np.zeros(len(self))
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)}) - ({len(other)})")
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.count)
60
+ pin = Point.full(pin, len(self))
70
61
  elif len(self) == 1 and len(pin) > 1:
71
- return self.full(len(pin)).offset(pin)
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)}) - ({len(pin)})")
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
- eul = Point.type_check(eul)
76
- # xyz-fixed Euler angle convention: matches ArduPilot AP_Math/Quaternion::from_euler
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
- pitch = np.arcsin(sinp)
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.array) -> Point:
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,:]]))#.remove_outliers(2) # Bodge to get rid of phase jump
195
+ return Point(np.vstack([ps.data, ps.data[-1,:]]))
194
196
 
195
- def body_diff(self, dt: np.array) -> Point:
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.ndarray) -> Quaternion:
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
- args = np.concatenate([args[0].data, args[1].data], axis=1)
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.8
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
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(TypeError):
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__().split("\n")
214
- assert rpr[0] == "ABC"
215
- assert np.all(rpr[1:] == str(p.to_pandas()).split("\n"))
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 = GPSPosition( 50.206, 4.1941755999999994)
18
- p0n = GPSPosition( 50.201, 4.1941755999999994)
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
- pytest.approx(diff.y, 0)
21
+ approx(diff.y, 0)
22
22
  assert diff.x > 0
23
23
 
24
- p0e= GPSPosition( 50.206, 4.195)
24
+ p0e= GPS( 50.206, 4.195, 0)
25
25
  diff= p0 - p0e # should be west vector
26
- pytest.approx(diff.x, 0)
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
- assert q1.to_axis_angle() == PZ(np.pi/4)
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
- np.testing.assert_array_almost_equal(
16
- ps.data,
17
- trans_from.translate(trans_to.translate(ps)).data
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
- np.testing.assert_array_almost_equal(
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