pfc-geometry 0.1.1__py3-none-any.whl → 0.2.5__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.
- geometry/__init__.py +4 -2
- geometry/base.py +107 -33
- geometry/coordinate_frame.py +5 -1
- geometry/gps.py +14 -28
- geometry/mass.py +14 -1
- geometry/point.py +17 -26
- geometry/quaternion.py +62 -38
- geometry/time.py +40 -0
- geometry/transformation.py +21 -5
- pfc_geometry-0.2.5.dist-info/METADATA +29 -0
- pfc_geometry-0.2.5.dist-info/RECORD +15 -0
- {pfc_geometry-0.1.1.dist-info → pfc_geometry-0.2.5.dist-info}/WHEEL +1 -1
- geometry/circle.py +0 -28
- pfc_geometry-0.1.1.dist-info/COPYING +0 -674
- pfc_geometry-0.1.1.dist-info/METADATA +0 -18
- pfc_geometry-0.1.1.dist-info/RECORD +0 -16
- {pfc_geometry-0.1.1.dist-info → pfc_geometry-0.2.5.dist-info}/LICENSE +0 -0
- {pfc_geometry-0.1.1.dist-info → pfc_geometry-0.2.5.dist-info}/top_level.txt +0 -0
geometry/__init__.py
CHANGED
|
@@ -9,6 +9,8 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
9
9
|
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
|
+
from .base import Base
|
|
13
|
+
from .time import Time
|
|
12
14
|
from .point import *
|
|
13
15
|
from .quaternion import *
|
|
14
16
|
from .gps import GPS
|
|
@@ -17,11 +19,11 @@ from .transformation import Transformation
|
|
|
17
19
|
from .mass import Mass
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
def Euler(*args, **kwargs):
|
|
22
|
+
def Euler(*args, **kwargs) -> Quaternion:
|
|
21
23
|
return Quaternion.from_euler(Point(*args, **kwargs))
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
def Euldeg(*args, **kwargs):
|
|
26
|
+
def Euldeg(*args, **kwargs) -> Quaternion:
|
|
25
27
|
return Quaternion.from_euler(Point(*args, **kwargs).radians())
|
|
26
28
|
|
|
27
29
|
|
geometry/base.py
CHANGED
|
@@ -10,8 +10,9 @@ 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
|
|
13
|
+
from typing import Self
|
|
14
14
|
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
15
16
|
import pandas as pd
|
|
16
17
|
from numbers import Number
|
|
17
18
|
|
|
@@ -47,11 +48,11 @@ class Base:
|
|
|
47
48
|
elif "data" in kwargs:
|
|
48
49
|
args = [kwargs["data"]]
|
|
49
50
|
else:
|
|
50
|
-
raise TypeError("unknown kwargs passed")
|
|
51
|
+
raise TypeError(f"unknown kwargs passed to {self.__class__.__name__}: {args}")
|
|
51
52
|
|
|
52
53
|
if len(args)==1:
|
|
53
|
-
if isinstance(args[0], np.ndarray): #data was passed directly
|
|
54
|
-
self.data = self.__class__._clean_data(args[0])
|
|
54
|
+
if isinstance(args[0], np.ndarray) or isinstance(args[0], list): #data was passed directly
|
|
55
|
+
self.data = self.__class__._clean_data(np.array(args[0]))
|
|
55
56
|
|
|
56
57
|
elif all([isinstance(a, self.__class__) for a in args[0]]):
|
|
57
58
|
#a list of self.__class__ is passed, concatenate into one
|
|
@@ -60,7 +61,7 @@ class Base:
|
|
|
60
61
|
elif isinstance(args[0], pd.DataFrame):
|
|
61
62
|
self.data = self.__class__._clean_data(np.array(args[0]))
|
|
62
63
|
else:
|
|
63
|
-
raise TypeError("unknown
|
|
64
|
+
raise TypeError(f"unknown args passed to {self.__class__.__name__}: {args[0]}")
|
|
64
65
|
|
|
65
66
|
elif len(args) == len(self.__class__.cols):
|
|
66
67
|
#three args passed, each represents a col
|
|
@@ -70,17 +71,18 @@ class Base:
|
|
|
70
71
|
self.data = self.__class__._clean_data(np.array(args).T)
|
|
71
72
|
elif all(isinstance(arg, list) for arg in args):
|
|
72
73
|
self.data = self.__class__._clean_data(np.array(args).T)
|
|
73
|
-
|
|
74
|
+
elif all(isinstance(arg, pd.Series) for arg in args):
|
|
75
|
+
self.data = self.__class__._clean_data(np.array(pd.concat(args, axis=1)))
|
|
74
76
|
else:
|
|
75
77
|
raise TypeError
|
|
76
78
|
else:
|
|
77
79
|
raise TypeError(f"Empty {self.__class__.__name__} not allowed")
|
|
78
80
|
|
|
79
81
|
@classmethod
|
|
80
|
-
def _clean_data(cls, data) -> np.
|
|
82
|
+
def _clean_data(cls, data) -> npt.NDArray[np.float64]:
|
|
81
83
|
assert isinstance(data, np.ndarray)
|
|
82
84
|
if data.dtype == 'O':
|
|
83
|
-
raise TypeError('data must have homogeneous shape')
|
|
85
|
+
raise TypeError(f'data must have homogeneous shape for {cls.__name__}, given {data.shape}')
|
|
84
86
|
if len(data.shape) == 1:
|
|
85
87
|
data = data.reshape(1, len(data))
|
|
86
88
|
|
|
@@ -102,21 +104,31 @@ class Base:
|
|
|
102
104
|
return a, b
|
|
103
105
|
|
|
104
106
|
@classmethod
|
|
105
|
-
def concatenate(cls, items):
|
|
107
|
+
def concatenate(cls, items) -> Self:
|
|
106
108
|
return cls(np.concatenate([i.data for i in items], axis=0))
|
|
107
109
|
|
|
108
|
-
def __getattr__(self, name):
|
|
110
|
+
def __getattr__(self, name) -> npt.NDArray[np.float64]:
|
|
109
111
|
if name in self.__class__.cols:
|
|
110
112
|
return self.data[:,self.__class__.cols.index(name)]
|
|
111
113
|
#return res[0] if len(res) == 1 else res
|
|
112
114
|
elif name in self.__class__.from_np + self.__class__.from_np_base:
|
|
113
115
|
return self.__class__(getattr(np, name)(self.data))
|
|
116
|
+
else:
|
|
117
|
+
for col in self.__class__.cols:
|
|
118
|
+
if len(name) > len(col):
|
|
119
|
+
if name[:len(col)] == col:
|
|
120
|
+
try:
|
|
121
|
+
id = int(name[len(col):])
|
|
122
|
+
except ValueError:
|
|
123
|
+
break
|
|
124
|
+
return getattr(self, col)[id]
|
|
125
|
+
|
|
114
126
|
raise AttributeError(f"Cannot get attribute {name}")
|
|
115
127
|
|
|
116
128
|
def __dir__(self):
|
|
117
129
|
return self.__class__.cols
|
|
118
130
|
|
|
119
|
-
def __getitem__(self, sli):
|
|
131
|
+
def __getitem__(self, sli) -> Self:
|
|
120
132
|
return self.__class__(self.data[sli,:])
|
|
121
133
|
|
|
122
134
|
def _dprep(self, other):
|
|
@@ -146,78 +158,86 @@ class Base:
|
|
|
146
158
|
else:
|
|
147
159
|
raise ValueError(f"unhandled datatype ({other.__class__.name})")
|
|
148
160
|
|
|
149
|
-
def radians(self):
|
|
161
|
+
def radians(self) -> Self:
|
|
150
162
|
return self.__class__(np.radians(self.data))
|
|
151
163
|
|
|
152
|
-
def degrees(self):
|
|
164
|
+
def degrees(self) -> Self:
|
|
153
165
|
return self.__class__(np.degrees(self.data))
|
|
154
166
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def count(self):
|
|
167
|
+
def count(self) -> int:
|
|
158
168
|
return len(self)
|
|
159
169
|
|
|
160
|
-
def __len__(self):
|
|
170
|
+
def __len__(self) -> int:
|
|
161
171
|
return self.data.shape[0]
|
|
162
172
|
|
|
173
|
+
@property
|
|
174
|
+
def ends(self) -> Self:
|
|
175
|
+
return self.__class__(self.data[[0,-1], :])
|
|
176
|
+
|
|
163
177
|
@dprep
|
|
164
178
|
def __eq__(self, other):
|
|
165
179
|
return np.all(self.data == other)
|
|
166
180
|
|
|
167
181
|
@dprep
|
|
168
|
-
def __add__(self, other):
|
|
182
|
+
def __add__(self, other) -> Self:
|
|
169
183
|
return self.__class__(self.data + other)
|
|
170
184
|
|
|
171
185
|
@dprep
|
|
172
|
-
def __radd__(self, other):
|
|
186
|
+
def __radd__(self, other) -> Self:
|
|
173
187
|
return self.__class__(other + self.data)
|
|
174
188
|
|
|
175
189
|
@dprep
|
|
176
|
-
def __sub__(self, other):
|
|
190
|
+
def __sub__(self, other) -> Self:
|
|
177
191
|
return self.__class__(self.data - other)
|
|
178
192
|
|
|
179
193
|
@dprep
|
|
180
|
-
def __rsub__(self, other):
|
|
194
|
+
def __rsub__(self, other) -> Self:
|
|
181
195
|
return self.__class__(other - self.data)
|
|
182
196
|
|
|
183
197
|
@dprep
|
|
184
|
-
def __mul__(self, other):
|
|
198
|
+
def __mul__(self, other) -> Self:
|
|
185
199
|
return self.__class__(self.data * other)
|
|
186
200
|
|
|
187
201
|
@dprep
|
|
188
|
-
def __rmul__(self, other):
|
|
202
|
+
def __rmul__(self, other) -> Self:
|
|
189
203
|
return self.__class__(other * self.data)
|
|
190
204
|
|
|
191
205
|
@dprep
|
|
192
|
-
def __rtruediv__(self, other):
|
|
206
|
+
def __rtruediv__(self, other) -> Self:
|
|
193
207
|
return self.__class__(other / self.data)
|
|
194
208
|
|
|
195
209
|
@dprep
|
|
196
|
-
def __truediv__(self, other):
|
|
210
|
+
def __truediv__(self, other) -> Self:
|
|
197
211
|
return self.__class__(self.data / other)
|
|
198
212
|
|
|
199
213
|
def __str__(self):
|
|
200
|
-
|
|
214
|
+
means = ' '.join(f'{c}_={v}' for c, v in zip(self.cols, np.mean(self.data, axis=0).round(2)))
|
|
215
|
+
return f'{self.__class__.__name__}({means}, len={len(self)})'
|
|
201
216
|
|
|
202
217
|
def __abs__(self):
|
|
203
218
|
return np.linalg.norm(self.data, axis=1)
|
|
219
|
+
|
|
220
|
+
def abs(self) -> Self:
|
|
221
|
+
return self.__class__(np.abs(self.data))
|
|
204
222
|
|
|
205
|
-
def __neg__(self):
|
|
223
|
+
def __neg__(self) -> Self:
|
|
206
224
|
return self.__class__(-self.data)
|
|
207
225
|
|
|
208
226
|
@dprep
|
|
209
|
-
def dot(self, other):
|
|
227
|
+
def dot(self, other: Self) -> Self:
|
|
210
228
|
return np.einsum('ij,ij->i', self.data, other)
|
|
211
229
|
|
|
212
|
-
def diff(self, dt:np.array):
|
|
230
|
+
def diff(self, dt:np.array) -> Self:
|
|
231
|
+
if not pd.api.types.is_list_like(dt):
|
|
232
|
+
dt = np.full(len(self), dt)
|
|
213
233
|
assert len(dt) == len(self)
|
|
214
234
|
return self.__class__(
|
|
215
235
|
np.gradient(self.data,axis=0) \
|
|
216
|
-
|
|
236
|
+
/ \
|
|
217
237
|
np.tile(dt, (len(self.__class__.cols),1)).T)
|
|
218
238
|
|
|
219
239
|
def to_pandas(self, prefix='', suffix='', columns=None, index=None):
|
|
220
|
-
if
|
|
240
|
+
if columns is not None:
|
|
221
241
|
cols = columns
|
|
222
242
|
else:
|
|
223
243
|
cols = [prefix + col + suffix for col in self.__class__.cols]
|
|
@@ -227,7 +247,7 @@ class Base:
|
|
|
227
247
|
index=index
|
|
228
248
|
)
|
|
229
249
|
|
|
230
|
-
def tile(self, count):
|
|
250
|
+
def tile(self, count) -> Self:
|
|
231
251
|
return self.__class__(np.tile(self.data, (count, 1)))
|
|
232
252
|
|
|
233
253
|
def to_dict(self):
|
|
@@ -235,6 +255,17 @@ class Base:
|
|
|
235
255
|
return {key: getattr(self, key)[0] for key in self.cols}
|
|
236
256
|
else:
|
|
237
257
|
return {key: getattr(self, key) for key in self.cols}
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def from_dict(Cls, data):
|
|
261
|
+
return Cls(**data)
|
|
262
|
+
|
|
263
|
+
def to_dicts(self):
|
|
264
|
+
return self.to_pandas().to_dict('records')
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def from_dicts(Cls, data: dict):
|
|
268
|
+
return Cls(pd.DataFrame.from_dict(data))
|
|
238
269
|
|
|
239
270
|
@classmethod
|
|
240
271
|
def full(cls, val, count):
|
|
@@ -257,4 +288,47 @@ class Base:
|
|
|
257
288
|
return self.__class__(np.cumsum(self.data,axis=0))
|
|
258
289
|
|
|
259
290
|
def round(self, decimals=0):
|
|
260
|
-
return self.__class__(self.data.round(decimals))
|
|
291
|
+
return self.__class__(self.data.round(decimals))
|
|
292
|
+
|
|
293
|
+
def __repr__(self):
|
|
294
|
+
return str(self)
|
|
295
|
+
|
|
296
|
+
def copy(self):
|
|
297
|
+
return self.__class__(self.data.copy())
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def unwrap(self, discont=np.pi):
|
|
301
|
+
return self.__class__(np.unwrap(self.data, discont=discont, axis=0))
|
|
302
|
+
|
|
303
|
+
def filter(self, order, cutoff, ts: np.ndarray=None):
|
|
304
|
+
from scipy.signal import butter, freqz, filtfilt
|
|
305
|
+
if ts is None:
|
|
306
|
+
ts = np.array(range(len(self)))
|
|
307
|
+
N = len(self)
|
|
308
|
+
T = (ts[-1] - ts[0]) / N
|
|
309
|
+
|
|
310
|
+
fs = 1/T
|
|
311
|
+
b, a = butter(
|
|
312
|
+
order,
|
|
313
|
+
cutoff,
|
|
314
|
+
fs=fs,
|
|
315
|
+
btype='low', analog=False
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return self.__class__(filtfilt(b, a, self.data, axis=0))
|
|
319
|
+
|
|
320
|
+
def fft(self, ts: np.ndarray=None):
|
|
321
|
+
from scipy.fft import fft, fftfreq
|
|
322
|
+
if ts is None:
|
|
323
|
+
ts = np.array(range(len(self)))
|
|
324
|
+
N = len(self)*2
|
|
325
|
+
T = (ts[-1] - ts[0]) / len(self)
|
|
326
|
+
|
|
327
|
+
yf = fft(self.data, axis=0, n=N)
|
|
328
|
+
xf = fftfreq(N, T)[:N//2]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
y=2.0/N * np.abs(yf[0:N//2, :])
|
|
332
|
+
|
|
333
|
+
return pd.DataFrame(np.column_stack([xf, y]), columns=['freq'] + self.cols).set_index('freq')
|
|
334
|
+
|
geometry/coordinate_frame.py
CHANGED
|
@@ -19,7 +19,7 @@ from geometry.base import Base
|
|
|
19
19
|
|
|
20
20
|
class Coord(Base):
|
|
21
21
|
cols = [
|
|
22
|
-
"ox", "oy", "
|
|
22
|
+
"ox", "oy", "oz",
|
|
23
23
|
"x1", "y1", "z1",
|
|
24
24
|
"x2", "y2", "z2",
|
|
25
25
|
"x3", "y3", "z3",
|
|
@@ -42,6 +42,10 @@ class Coord(Base):
|
|
|
42
42
|
z.unit().data
|
|
43
43
|
],axis=1))
|
|
44
44
|
|
|
45
|
+
@staticmethod
|
|
46
|
+
def zero(count=1):
|
|
47
|
+
return Coord.from_nothing(count)
|
|
48
|
+
|
|
45
49
|
@staticmethod
|
|
46
50
|
def from_nothing(count=1):
|
|
47
51
|
return Coord.from_axes(P0(count), PX(1,count), PY(1,count), PZ(1,count))
|
geometry/gps.py
CHANGED
|
@@ -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)
|
geometry/mass.py
CHANGED
|
@@ -33,6 +33,10 @@ class Mass(Base):
|
|
|
33
33
|
def matrix(self):
|
|
34
34
|
return self.data[:,1:].reshape((len(self), 3, 3))
|
|
35
35
|
|
|
36
|
+
@property
|
|
37
|
+
def I(self):
|
|
38
|
+
return self.matrix()
|
|
39
|
+
|
|
36
40
|
def offset(self, v: Point):
|
|
37
41
|
xx = v.y**2 + v.z**2
|
|
38
42
|
yy = v.z**2 + v.x**2
|
|
@@ -46,5 +50,14 @@ class Mass(Base):
|
|
|
46
50
|
]))
|
|
47
51
|
) + self
|
|
48
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
|
+
|
|
49
63
|
|
|
50
|
-
|
geometry/point.py
CHANGED
|
@@ -9,10 +9,10 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
9
9
|
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
|
+
from __future__ import annotations
|
|
12
13
|
from .base import Base
|
|
13
14
|
import numpy as np
|
|
14
15
|
import pandas as pd
|
|
15
|
-
from typing import List
|
|
16
16
|
from warnings import warn
|
|
17
17
|
|
|
18
18
|
|
|
@@ -23,15 +23,13 @@ class Point(Base):
|
|
|
23
23
|
"arcsin","arccos","arctan",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
def scale(self, value):
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
res[
|
|
30
|
-
|
|
26
|
+
def scale(self, value) -> Point:
|
|
27
|
+
with np.errstate(divide="ignore"):
|
|
28
|
+
res = value/abs(self)
|
|
29
|
+
res[res==np.inf] = 0
|
|
30
|
+
return self * res
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def unit(self):
|
|
32
|
+
def unit(self) -> Point:
|
|
35
33
|
return self.scale(1)
|
|
36
34
|
|
|
37
35
|
def remove_outliers(self, nstds = 2):
|
|
@@ -43,7 +41,7 @@ class Point(Base):
|
|
|
43
41
|
|
|
44
42
|
data[abs(ab - mean) > nstds * std, :] = [np.nan, np.nan, np.nan]
|
|
45
43
|
|
|
46
|
-
return Point(pd.DataFrame(data).
|
|
44
|
+
return Point(pd.DataFrame(data).ffill().bfill().to_numpy())
|
|
47
45
|
|
|
48
46
|
def mean(self):
|
|
49
47
|
return Point(np.mean(self.data, axis=0))
|
|
@@ -57,6 +55,9 @@ class Point(Base):
|
|
|
57
55
|
def angles(self, p2):
|
|
58
56
|
return (self.cross(p2) / (abs(self) * abs(p2))).arcsin
|
|
59
57
|
|
|
58
|
+
def planar_angles(self):
|
|
59
|
+
return Point(np.arctan2(self.y, self.z), np.arctan2(self.z, self.x), np.arctan2(self.x, self.y))
|
|
60
|
+
|
|
60
61
|
def angle(self, p2):
|
|
61
62
|
return abs(Point.angles(self, p2))
|
|
62
63
|
|
|
@@ -141,31 +142,27 @@ def cross(a, b) -> Point:
|
|
|
141
142
|
|
|
142
143
|
@ppmeth
|
|
143
144
|
def cos_angle_between(a: Point, b: Point) -> np.ndarray:
|
|
144
|
-
if a == 0 or b == 0:
|
|
145
|
-
raise ValueError("cannot measure the angle to a zero length vector")
|
|
146
145
|
return a.unit().dot(b.unit())
|
|
147
146
|
|
|
148
|
-
|
|
149
147
|
@ppmeth
|
|
150
148
|
def angle_between(a: Point, b: Point) -> np.ndarray:
|
|
151
149
|
return np.arccos(a.cos_angle_between(b))
|
|
152
150
|
|
|
153
151
|
@ppmeth
|
|
154
152
|
def scalar_projection(a: Point, b: Point) -> Point:
|
|
155
|
-
if a==0 or b==0:
|
|
156
|
-
return 0
|
|
157
153
|
return a.cos_angle_between(b) * abs(a)
|
|
158
154
|
|
|
159
155
|
@ppmeth
|
|
160
156
|
def vector_projection(a: Point, b: Point) -> Point:
|
|
161
|
-
if abs(a) == 0:
|
|
162
|
-
return Point.zeros()
|
|
163
157
|
return b.scale(a.scalar_projection(b))
|
|
164
158
|
|
|
159
|
+
@ppmeth
|
|
160
|
+
def vector_rejection(a: Point, b: Point) -> Point:
|
|
161
|
+
return a - ((Point.dot(a, b)) / Point.dot(b,b)) * b
|
|
162
|
+
|
|
163
|
+
|
|
165
164
|
@ppmeth
|
|
166
165
|
def is_parallel(a: Point, b: Point, tolerance=1e-6):
|
|
167
|
-
if a.unit() == b.unit():
|
|
168
|
-
return True
|
|
169
166
|
return abs(a.cos_angle_between(b) - 1) < tolerance
|
|
170
167
|
|
|
171
168
|
@ppmeth
|
|
@@ -177,14 +174,8 @@ def min_angle_between(p1: Point, p2: Point):
|
|
|
177
174
|
angle = angle_between(p1, p2) % np.pi
|
|
178
175
|
return min(angle, np.pi - angle)
|
|
179
176
|
|
|
180
|
-
@ppmeth
|
|
181
|
-
def angle_between(a: Point, b: Point) -> float:
|
|
182
|
-
return np.arccos(cos_angle_between(a, b))
|
|
183
|
-
|
|
184
177
|
def arbitrary_perpendicular(v: Point) -> Point:
|
|
185
|
-
|
|
186
|
-
return Point(0, 1, 0)
|
|
187
|
-
return Point(-v.y, v.x, 0).unit
|
|
178
|
+
return Point(-v.y, v.x, 0).unit()
|
|
188
179
|
|
|
189
180
|
def vector_norm(point: Point):
|
|
190
181
|
return abs(point)
|